La tua idea di Monoid
è un buon inizio, ma Monoid
non è abbastanza generale. Quello di cui hai veramente bisogno è un Category
. Potresti crearne uno personalizzato per il lavoro, ma puoi anche evitare di pensare alle categorie semplicemente rinunciando all'idea di avere un singolo tipo fisso per rappresentare un record parziale e invece lasciare che i record parziali con campi diversi presenti abbiano tipi diversi. Questo significa che stai lavorando con la categoria di tipi e funzioni, a volte chiamate Hask, ma non devi pensarci. Avvertenza: uno dei fantastici pacchetti di record su Hackage probabilmente rende questo genere di cose molto più facile da fare, ma io non (ancora) capisco nessuno di loro abbastanza bene da usarli, per non parlare di raccomandarli.
Prima della caldaia-piastra
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleInstances #-}
module PartialRec where
import Data.Proxy
Ora i tipi. PList
rappresenta un record (parziale). Il primo elenco di argomenti rappresenta i tipi del campo del record, mentre il secondo elenco di argomenti indica quali campi sono presenti.
-- 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)
Usiamo una famiglia tipo di esprimere come i tipi combinano quando due record parziali sono combinati. In particolare, qualsiasi risultato presente nel record parziale sarà presente nel risultato.
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
La funzione combine
combina due record parziali per formare uno nuovo con gli stessi tipi di campo ed eventuali campi presenti in entrambi atto parziale. Se un campo è presente in entrambi i record, viene scelto il primo.
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)
La logica inadempiente è lasciato al buildRec
. buildRec
prende un record parziale con un set sufficiente di campi e produce valori per i campi facoltativi in base ai campi obbligatori e ai campi facoltativi effettivamente presenti. buildRec
viene effettivamente implementato utilizzando una classe di tipi con istanze scelte da una famiglia di tipi, per supportare più set di campi sufficienti.
-- 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)
Ecco alcune funzioni per creare record parziali con un singolo campo ciascuno.
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
Ecco alcuni esempi di utilizzo. useChar
terminerà utilizzando l'istanza Builder2
, mentre noChar
utilizzerà l'istanza 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
Se hai veramente bisogno di valori predefiniti parziali, non penso che ci sia una soluzione ordinata. Se si desidera la sicurezza in fase di compilazione, è necessario portare la distinzione a livello di tipo, attraverso entrambe (in ordine crescente di disordine) le funzioni di costruzione di Foo con diverse combinazioni di argomenti, un corrispondente tipo di somma 'PreFoo' con un costruttore per default parziale o più tipi pre-Foo separati che servono allo stesso scopo. – duplode