您对Monoid 的想法是一个好的开始,但Monoid 还不够笼统。你真正需要的是Category。您可以为工作定制一个,但您甚至可以通过放弃使用单个固定类型来表示部分记录的想法,而是让存在不同字段的部分记录具有不同类型,从而避免考虑类别。这意味着您正在使用类型和函数的类别,有时称为 Hask,但您不必考虑这一点。警告:Hackage 上的一个花哨的记录包可能使这类事情更容易做,但我(还)没有充分理解它们中的任何一个来使用它们,更不用说推荐它们了。
首先是样板
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleInstances #-}
module PartialRec where
import Data.Proxy
现在是类型。 PList 表示(部分)记录。它的第一个参数列表表示记录字段的类型,而第二个参数列表表示存在哪些字段。
-- Skip is totally unnecessary, but makes the
-- syntax of skips a bit less horrible.
data Skip = Skip
infixr 6 `PCons`, `PSkip`
data PList :: [*] -> [Bool] -> * where
PNil :: PList '[] '[]
PCons :: a -> PList as bs -> PList (a ': as) ('True ': bs)
PSkip :: Skip -> PList as bs -> PList (a ': as) ('False ': bs)
我们使用类型族来表达当两个部分记录组合时类型是如何组合的。特别是,任何部分记录中存在的任何字段都将出现在结果中。
type family Combine (as :: [Bool]) (bs :: [Bool]) :: [Bool] where
Combine '[] '[] = '[]
Combine ('True ': xs) (y ': ys) = 'True ': Combine xs ys
Combine ('False ': xs) (y ': ys) = y ': Combine xs ys
combine 函数将两个部分记录组合成一个具有相同字段类型和任一部分记录中存在的任何字段的新记录。如果两个记录中都存在一个字段,则选择第一个。
combine :: PList as bs -> PList as cs -> PList as (Combine bs cs)
combine PNil PNil = PNil
combine (PCons x xs) (PSkip _ ys) = PCons x (combine xs ys)
combine (PSkip _ xs) (PCons y ys) = PCons y (combine xs ys)
combine (PSkip _ xs) (PSkip _ ys) = PSkip Skip (combine xs ys)
combine (PCons x xs) (PCons _ ys) = PCons x (combine xs ys)
默认逻辑留给buildRec。 buildRec 获取具有足够字段集的部分记录,并根据必填字段和实际存在的任何可选字段为可选字段生成值。 buildRec 实际上是使用类型类实现的,其实例由类型族选择,以支持多个足够的字段集。
-- Names for instances
data BuilderTag = Builder1 | Builder2
-- Given a list of types present, determines
-- the correct Builder instance to use.
type family ChooseBuilder (present :: [Bool]) :: BuilderTag where
ChooseBuilder '[ 'True, 'True, 'True, b3 ] = Builder2
ChooseBuilder '[ 'True, b1, 'True, b2 ] = Builder1
class Builder (tag :: BuilderTag) (present :: [Bool]) where
buildRec' :: proxy tag -> PList '[Int, Char, Bool, Integer] present -> (Int, Char, Bool, Integer)
buildRec :: forall tag present . (Builder tag present, tag ~ ChooseBuilder present)
=> PList '[Int, Char, Bool, Integer] present -> (Int, Char, Bool, Integer)
buildRec xs = buildRec' (Proxy :: Proxy tag) xs
instance Builder 'Builder1 '[ 'True, b1, 'True, b2 ] where
buildRec' _ (i `PCons` Skip `PSkip` b `PCons` Skip `PSkip` PNil) = (i, toEnum (i + fromEnum b) , b, if i > 3 && b then 12 else 13)
buildRec' _ (i `PCons` Skip `PSkip` b `PCons` intg `PCons` PNil) = (i, toEnum (i + fromEnum b + fromIntegral intg), b, intg)
buildRec' _ (i `PCons` c `PCons` b `PCons` Skip `PSkip` PNil) = (i, c, b, fromIntegral i)
buildRec' _ (i `PCons` c `PCons` b `PCons` intg `PCons` PNil) = (i, c, b, intg)
instance Builder 'Builder2 '[ 'True, 'True, 'True, b3 ] where
buildRec' _ (i `PCons` c `PCons` b `PCons` Skip `PSkip` PNil) = (i, c, b, fromIntegral i)
buildRec' _ (i `PCons` c `PCons` b `PCons` intg `PCons` PNil) = (i, c, b, intg)
这里有一些函数可以构建部分记录,每个记录都有一个字段。
justInt :: Int -> PList '[Int, a, b, c] '[ 'True, 'False, 'False, 'False]
justInt x = x `PCons` Skip `PSkip` Skip `PSkip` Skip `PSkip` PNil
justChar :: Char -> PList '[a, Char, b, c] '[ 'False, 'True, 'False, 'False]
justChar x = Skip `PSkip` x `PCons` Skip `PSkip` Skip `PSkip` PNil
justBool :: Bool -> PList '[a, b, Bool, c] '[ 'False, 'False, 'True, 'False]
justBool x = Skip `PSkip` Skip `PSkip` x `PCons` Skip `PSkip` PNil
justInteger :: Integer -> PList '[a, b, c, Integer] '[ 'False, 'False, 'False, 'True]
justInteger x = Skip `PSkip` Skip `PSkip` Skip `PSkip` x `PCons` PNil
以下是一些示例用途。 useChar 最终将使用 Builder2 实例,而 noChar 将使用 Builder1 实例。
useChar :: (Int, Char, Bool, Integer)
useChar = buildRec $ justInt 12 `combine` justBool False `combine` justChar 'c'
noChar :: (Int, Char, Bool, Integer)
noChar = buildRec $ justInt 12 `combine` justBool False