Un vantaggio è che le famiglie di dati sono iniettabili, a differenza delle famiglie di tipi.
Se hai
type family TF a
data family DF a
poi si sa che DF a ~ DF b
implica che a ~ b
, mentre con il TF, non è necessario - per qualsiasi a
si può essere sicuri che DF a
è un tipo completamente nuovo (proprio come [a]
è un tipo diverso da [b]
, a meno che, naturalmente, non sia a ~ b
, mentre una famiglia di tipi può mappare più tipi di input sullo stesso tipo esistente.
Un secondo è che le famiglie di dati possono essere parzialmente applicate, come qualsiasi altro costruttore di tipi, mentre le famiglie di tipi non possono.
Questo non è un esempio particolarmente nel mondo reale, ma per esempio, si può fare:
data instance DF Int = DInt Int
data instance DF String = DString String
class C t where
foo :: t Int -> t String
instance C DF where -- notice we are using DF without an argument
-- notice also that you can write instances for data families at all,
-- unlike type families
foo (DInt i) = DString (show i)
In sostanza, DF
e DF a
sono reali, di prima classe, i tipi legittimi, di per sé, come ogni altra digita la tua dichiarazione con data
. TF a
è solo un modulo intermedio che valuta un tipo.
Ma suppongo che tutto ciò non sia molto illuminante, o almeno non lo era per me, quando mi chiedevo delle famiglie di dati e leggevo cose simili.
Ecco la regola generale passo. Ogni volta che ti ritrovi a ripetere lo schema di una famiglia di tipi e per ogni tipo di input, dichiari un nuovo tipo data
per la famiglia di tipi su cui mappare, è preferibile ritagliare l'intermediario e utilizzare invece una famiglia di dati.
Un esempio del mondo reale dalla libreria vector. vector
ha diversi tipi di vettori: vettori in scatola, vettori non in scatola, vettori primitivi, vettori memorizzabili. Per ogni tipo di Vector
è presente un tipo corrispondente, mutabile MVector
(i normali vettori non sono modificabili). Così si presenta così:
type family Mutable v :: * -> * -> * -- the result type has two type parameters
module Data.Vector{.Mutable} where
data Vector a = ...
data MVector s a = ...
type instance Mutable Vector = MVector
module Data.Vector.Storable{.Mutable} where
data Vector a = ...
data MVector s a = ...
type instance Mutable Vector = MVector
[etc.]
Ora, invece di quello, avrei preferito:
data family Mutable v :: * -> * -> *
module Data.Vector{.Mutable} where
data Vector a = ...
data instance Mutable Vector s a = ...
type MVector = Mutable Vector
module Data.Vector.Storable{.Mutable} where
data Vector a = ...
data instance Mutable Vector s a = ...
type MVector = Mutable Vector
[etc.]
che codifica per l'invariante che per ogni tipo di Vector
c'è esattamente un Mutable Vector
tipo, e che non c'è una corrispondenza uno-a-uno tra di loro. La versione mutevole di un è sempre chiamata Vector
: questo è il suo nome e non ha altro. Se si dispone di un Mutable Vector
, è possibile ottenere il tipo di immutabile corrispondente Vector
, perché è proprio lì come un argomento di tipo. Con type family Mutable
, una volta applicato a un argomento, viene valutato un tipo di risultato non specificato (presumibilmente chiamato MVector
, ma non si può sapere) e non è possibile eseguire il mapping all'indietro.
Invece di puntare semplicemente a un tipo esistente, a volte si desidera anche avere il proprio tipo fresco, avvolgendo uno esistente o solo un normale record con vari campi, ma non solo un sinonimo nudo di uno esistente. Puoi usarli nello stesso modo in cui usi normalmente i tipi dichiarati con "data", tranne per il fatto che questa volta la rappresentazione sarà diversa a seconda dell'argomento del tipo che gli darai. –