Je ne sais pas trop comment choisir entre data family
et type family
. La page wiki sur TypeFamilies va dans beaucoup de détails. Parfois, il se réfère officieusement à data family
comme une "famille type" en prose, mais bien sûr il y a aussi type family
à Haskell.
Il existe un exemple simple qui montre où deux versions de code sont affichées, ne différant que si un data family
ou un type family
est déclaré:
-- BAD: f is too ambiguous, due to non-injectivity
-- type family F a
-- OK
data family F a
f :: F a -> F a
f = undefined
g :: F Int -> F Int
g x = f x
type
et data
ont ici la même signification, mais le type family
la version ne parvient pas à vérifier le type, tandis que le data family
la version est correcte, car data family
"crée de nouveaux types et est donc injectif" (dit la page wiki).
Ma leçon à retenir de tout cela est "essayez data family
pour les cas simples et, s'il n'est pas assez puissant, essayez type family
". Ce qui est bien, mais j'aimerais mieux le comprendre. Y a-t-il un diagramme de Venn ou un arbre de décision que je peux suivre pour distinguer quand utiliser quoi?
Je ne pense pas qu'il y aura d'arbre de décision ou de diagramme de Venn parce que les applications pour les familles de types et de données sont assez larges.
En général, vous avez déjà souligné les principales différences de conception et je serais d'accord avec votre point de départ pour voir d'abord si vous pouvez vous en sortir avec data family
.
Pour moi, le point clé est que chaque instance d'un data family
crée un nouveau type, ce qui limite considérablement la puissance car vous ne pouvez pas faire ce qui est souvent la chose la plus naturelle et faire d'un type existant l'instance.
Par exemple, l'exemple de GMapKey
sur la page wiki Haskell sur les "types indexés" est un ajustement assez naturel pour les familles de données:
class GMapKey k where
data GMap k :: * -> *
empty :: GMap k v
lookup :: k -> GMap k v -> Maybe v
insert :: k -> v -> GMap k v -> GMap k v
Le type de clé de la carte k
est l'argument de la famille de données et le type de carte réel est le résultat de la famille de données (GMap k
). En tant qu'utilisateur d'une instance de GMapKey
, vous êtes probablement très satisfait de la GMap k
tapez pour être abstrait pour vous et il suffit de le manipuler via les opérations de carte génériques dans la classe type.
En revanche, l'exemple Collects
sur la même page wiki est en quelque sorte l'inverse:
class Collects ce where
type Elem ce
empty :: ce
insert :: Elem ce -> ce -> ce
member :: Elem ce -> ce -> Bool
toList :: ce -> [Elem ce]
Le type d'argument est la collection et le type de résultat est l'élément de la collection. En général, un utilisateur va vouloir opérer sur ces éléments directement en utilisant les opérations normales sur ce type. Par exemple, la collection peut être IntSet
et l'élément peut être Int
. Avoir le Int
enveloppé dans un autre type serait assez gênant.
Remarque - ces deux exemples concernent des classes de type et n'ont donc pas besoin du mot clé family
car déclarer un type à l'intérieur d'une classe de type implique qu'il doit s'agir d'une famille. Exactement les mêmes considérations s'appliquent cependant que pour les familles autonomes, c'est juste une question de la façon dont l'abstraction est organisée.
(Renforcer les informations utiles des commentaires dans une réponse.)
Deux façons syntaxiquement différentes de déclarer une famille de types et/ou famille de données , qui sont sémantiquement équivalents:
autonome:
type family Foo data family Bar
ou dans le cadre d'une classe de types:
class C where
type Foo
data Bar
déclarent tous deux une famille de types, mais à l'intérieur d'une classe de types, la partie family
est impliquée par le contexte class
, donc GHC/Haskell abrège la déclaration.
data family F
Crée un nouveau type, semblable à la façon dont data F = ...
Crée un nouveau type.
type family F
Ne crée pas de nouveau type, semblable à la façon dont type F = Bar Baz
Ne crée pas de nouveau type (il crée simplement un alias/synonyme d'un type existant).
type family
Un exemple (légèrement modifié) de Data.MonoTraversable.Element
:
import Data.ByteString as S
import Data.ByteString.Lazy as L
-- Declare a family of type synonyms, called `Element`
-- `Element` has kind `* -> *`; it takes one parameter, which we call `container`
type family Element container
-- ByteString is a container for Word8, so...
-- The Element of a `S.ByteString` is a `Word8`
type instance Element S.ByteString = Word8
-- and the Element of a `L.ByteString` is also `Word8`
type instance Element L.ByteString = Word8
Dans une famille de types, le côté droit des équations Word8
Nomme un type existant; les choses sont le côté gauche crée de nouveaux synonymes: Element S.ByteString
et Element L.ByteString
Avoir un synonyme signifie que nous pouvons échanger Element Data.ByteString
Avec Word8
:
-- `w` is a Word8....
>let w = 0::Word8
-- ... and also an `Element L.ByteString`
>:t w :: Element L.ByteString
w :: Element L.ByteString :: Word8
-- ... and also an `Element S.ByteString`
>:t w :: Element S.ByteString
w :: Element S.ByteString :: Word8
-- but not an `Int`
>:t w :: Int
Couldn't match expected type `Int' with actual type `Word8'
Ces synonymes de type sont "non injectifs" ("unidirectionnels"), et donc non inversibles.
-- As before, `Word8` matches `Element L.ByteString` ...
>(0::Word8)::(Element L.ByteString)
-- .. but GHC can't infer which `a` is the `Element` of (`L.ByteString` or `S.ByteString` ?):
>(w)::(Element a)
Couldn't match expected type `Element a'
with actual type `Element a0'
NB: `Element' is a type function, and may not be injective
The type variable `a0' is ambiguous
Pire encore, GHC ne peut même pas résoudre les cas non ambigus!:
type instance Element Int = Bool
> True::(Element a)
> NB: `Element' is a type function, and may not be injective
Notez l'utilisation de "peut-être pas"! Je pense que GHC est conservateur et refuse de vérifier si le Element
est vraiment injectif. (Peut-être parce qu'un programmeur pourrait ajouter un autre type instance
Plus tard, après avoir importé un module précompilé, ajoutant de l'ambiguïté.
data family
En revanche: dans une famille de données, chaque côté droit contient un constructeur unique, donc les définitions sont des équations injectives ("réversibles").
-- Declare a list-like data family
data family XList a
-- Declare a list-like instance for Char
data instance XList Char = XCons Char (XList Char) | XNil
-- Declare a number-like instance for ()
data instance XList () = XListUnit Int
-- ERROR: "Multiple declarations of `XListUnit'"
data instance XList () = XListUnit Bool
-- (Note: GHCI accepts this; the new declaration just replaces the previous one.)
Avec data family
, Voir le nom du constructeur à droite (XCons
ou XListUnit
) est suffisant pour faire savoir à l'inférenceur de type que nous devons travailler avec XList ()
pas un XList Char
. Les noms de constructeurs étant uniques, ces définitions sont injectives/réversibles.
type
"juste" déclare un synonyme, pourquoi est-il sémantiquement utile?Habituellement, type
synonymes ne sont que des abréviations, mais type
les synonymes de la famille ont une puissance supplémentaire: ils peuvent faire qu'un type simple (kind *
) Devienne un synonyme d'un "type with kind * -> *
Appliqué à un argument ":
type instance F A = B
fait que B
correspond à F a
. Ceci est utilisé, par exemple, dans Data.MonoTraversable
Pour faire un type simple Word8
Faire correspondre les fonctions du type Element a -> a
(Element
est défini ci-dessus).
Par exemple, (un peu idiot), supposons que nous ayons une version de const
qui ne fonctionne qu'avec les types "connexes":
> class Const a where constE :: (Element a) -> a -> (Element a)
> instance Const S.ByteString where constE = const
> constE (0::Word8) undefined
ERROR: Couldn't match expected type `Word8' with actual type `Element a0'
-- By providing a match `a = S.ByteString`, `Word8` matches `(Element S.ByteString)`
> constE (0::Word8) (undefined::S.ByteString)
0
-- impossible, since `Char` cannot match `Element a` under our current definitions.
> constF 'x' undefined