web-dev-qa-db-fra.com

«famille de types» vs «famille de données», en bref?

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?

50
misterbee

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.

34
Ganesh Sittampalam

(Renforcer les informations utiles des commentaires dans une réponse.)

Déclaration autonome vs en classe

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.

"Nouveau type" vs "Synonyme de type"/"Alias ​​de type"

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).

Exemple de non-injectivité de 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é.

Exemple d'injectivité de 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.

Si 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 
57
misterbee