Type Haskell vs constructeur de données

124

J'apprends Haskell de Learnyouahaskell.com . J'ai du mal à comprendre les constructeurs de types et les constructeurs de données. Par exemple, je ne comprends pas vraiment la différence entre ceci:

data Car = Car { company :: String  
               , model :: String  
               , year :: Int  
               } deriving (Show) 

et ça:

data Car a b c = Car { company :: a  
                     , model :: b  
                     , year :: c   
                     } deriving (Show)  

Je comprends que la première consiste simplement à utiliser un constructeur ( Car) pour créer des données de type Car. Je ne comprends pas vraiment le second.

Aussi, comment les types de données définis comme ceci:

data Color = Blue | Green | Red

s'intégrer dans tout cela?

D'après ce que je comprends, le troisième exemple ( Color) est un type qui peut être dans trois états: Blue, Greenou Red. Mais cela entre en conflit avec la façon dont je comprends les deux premiers exemples: est-ce que le type Carne peut être que dans un seul état Car, qui peut prendre divers paramètres pour se construire? Si oui, comment le deuxième exemple s'intègre-t-il?

Essentiellement, je cherche une explication qui unifie les trois exemples / constructions de code ci-dessus.

Aristide
la source
18
Votre exemple de voiture peut être un peu déroutant car il Cars'agit à la fois d'un constructeur de type (sur le côté gauche du =) et d'un constructeur de données (sur le côté droit). Dans le premier exemple, le Carconstructeur de type n'accepte aucun argument, dans le second, il en prend trois. Dans les deux exemples, le Carconstructeur de données prend trois arguments (mais les types de ces arguments sont dans un cas fixes et dans l'autre paramétrés).
Simon Shine
le premier consiste simplement à utiliser un constructeur de données ( Car :: String -> String -> Int -> Car) pour créer des données de type Car. la seconde consiste simplement à utiliser un constructeur de données ( Car :: a -> b -> c -> Car a b c) pour créer des données de type Car a b c.
Will Ness

Réponses:

228

Dans une datadéclaration, un constructeur de type est la chose sur le côté gauche du signe égal. Le (s) constructeur (s) de données sont les éléments du côté droit du signe égal. Vous utilisez des constructeurs de type là où un type est attendu et vous utilisez des constructeurs de données là où une valeur est attendue.

Constructeurs de données

Pour simplifier les choses, nous pouvons commencer par un exemple de type qui représente une couleur.

data Colour = Red | Green | Blue

Ici, nous avons trois constructeurs de données. Colourest un type et Greenest un constructeur qui contient une valeur de type Colour. De même, Redet Bluesont tous deux des constructeurs qui construisent des valeurs de type Colour. Nous pourrions imaginer le pimenter cependant!

data Colour = RGB Int Int Int

Nous n'avons toujours que le type Colour, mais ce RGBn'est pas une valeur - c'est une fonction prenant trois Ints et renvoyant une valeur! RGBa le type

RGB :: Int -> Int -> Int -> Colour

RGBest un constructeur de données qui est une fonction prenant certaines valeurs comme arguments, puis les utilise pour construire une nouvelle valeur. Si vous avez effectué une programmation orientée objet, vous devez le reconnaître. En POO, les constructeurs prennent également certaines valeurs comme arguments et renvoient une nouvelle valeur!

Dans ce cas, si nous appliquons RGBà trois valeurs, nous obtenons une valeur de couleur!

Prelude> RGB 12 92 27
#0c5c1b

Nous avons construit une valeur de type Colouren appliquant le constructeur de données. Un constructeur de données contient une valeur comme le ferait une variable ou prend d'autres valeurs comme argument et crée une nouvelle valeur . Si vous avez déjà fait de la programmation, ce concept ne devrait pas vous être très étrange.

Entracte

Si vous voulez construire un arbre binaire pour stocker Strings, vous pouvez imaginer faire quelque chose comme

data SBTree = Leaf String
            | Branch String SBTree SBTree

Ce que nous voyons ici est un type SBTreequi contient deux constructeurs de données. En d'autres termes, il existe deux fonctions (à savoir Leafet Branch) qui construiront des valeurs de SBTreetype. Si vous n'êtes pas familier avec le fonctionnement des arbres binaires, accrochez-vous simplement. Vous n'avez pas vraiment besoin de savoir comment fonctionnent les arbres binaires, seulement que celui-ci stocke les Strings d'une manière ou d'une autre.

Nous voyons également que les deux constructeurs de données prennent un Stringargument - c'est la chaîne qu'ils vont stocker dans l'arborescence.

Mais! Et si nous voulions également pouvoir stocker Bool, nous devions créer un nouvel arbre binaire. Cela pourrait ressembler à ceci:

data BBTree = Leaf Bool
            | Branch Bool BBTree BBTree

Constructeurs de type

Les deux SBTreeet BBTreesont des constructeurs de type. Mais il y a un problème flagrant. Voyez-vous à quel point ils sont similaires? C'est le signe que vous voulez vraiment un paramètre quelque part.

Nous pouvons donc faire ceci:

data BTree a = Leaf a
             | Branch a (BTree a) (BTree a)

Maintenant, nous introduisons une variable de type en a tant que paramètre du constructeur de type. Dans cette déclaration, BTreeest devenue une fonction. Il prend un type comme argument et renvoie un nouveau type .

Il est important ici de considérer la différence entre un type concret (les exemples incluent Int, [Char]et Maybe Bool) qui est un type qui peut être attribué à une valeur dans votre programme, et une fonction de constructeur de type dont vous avez besoin pour alimenter un type pour pouvoir être assigné à une valeur. Une valeur ne peut jamais être de type "liste", car elle doit être une "liste de quelque chose ". Dans le même esprit, une valeur ne peut jamais être de type "arbre binaire", car elle doit être un "arbre binaire stockant quelque chose ".

Si nous passons, disons, Boolcomme argument à BTree, il renvoie le type BTree Bool, qui est un arbre binaire qui stocke Bools. Remplacez chaque occurrence de la variable de type apar le type Bool, et vous pourrez voir par vous-même comment c'est vrai.

Si vous le souhaitez, vous pouvez voir BTreecomme une fonction avec le genre

BTree :: * -> *

Les types sont un peu comme des types - le *indique un type concret, donc nous disons qu'il BTreeva d'un type concret à un type concret.

Emballer

Revenez ici un moment et prenez note des similitudes.

  • Un constructeur de données est une «fonction» qui prend 0 ou plusieurs valeurs et vous renvoie une nouvelle valeur.

  • Un constructeur de type est une "fonction" qui prend 0 ou plusieurs types et vous redonne un nouveau type.

Les constructeurs de données avec des paramètres sont sympas si nous voulons de légères variations dans nos valeurs - nous mettons ces variations dans les paramètres et laissons le créateur de la valeur décider quels arguments ils vont mettre. Dans le même sens, les constructeurs de types avec des paramètres sont cool si nous voulons de légères variations dans nos types! Nous mettons ces variations comme paramètres et laissons le type qui crée le type décider des arguments qu'il va mettre.

Une étude de cas

Comme la dernière ligne droite ici, nous pouvons considérer le Maybe atype. Sa définition est

data Maybe a = Nothing
             | Just a

Voici Maybeun constructeur de type qui renvoie un type concret. Justest un constructeur de données qui renvoie une valeur. Nothingest un constructeur de données qui contient une valeur. Si nous regardons le type de Just, nous voyons que

Just :: a -> Maybe a

En d'autres termes, Justprend une valeur de type aet renvoie une valeur de type Maybe a. Si nous regardons le genre de Maybe, nous voyons que

Maybe :: * -> *

En d'autres termes, Maybeprend un type concret et renvoie un type concret.

Encore une fois! La différence entre un type concret et une fonction constructeur de type. Vous ne pouvez pas créer une liste de Maybes - si vous essayez d'exécuter

[] :: [Maybe]

vous obtiendrez une erreur. Vous pouvez cependant créer une liste de Maybe Int, ou Maybe a. C'est parce que Maybec'est une fonction de constructeur de type, mais une liste doit contenir des valeurs d'un type concret. Maybe Intet Maybe asont des types concrets (ou si vous le souhaitez, des appels à des fonctions de constructeur de type qui renvoient des types concrets.)

kqr
la source
2
Dans votre premier exemple, RED GREEN et BLUE sont des constructeurs qui n'acceptent aucun argument.
OllieB
3
L'affirmation selon laquelle data Colour = Red | Green | Blue"nous n'avons aucun constructeur du tout" est totalement erronée. Les constructeurs de types et les constructeurs de données n'ont pas besoin de prendre d'arguments, voir par exemple haskell.org/haskellwiki/Constructor qui souligne que dans data Tree a = Tip | Node a (Tree a) (Tree a)«il y a deux constructeurs de données, Tip et Node».
Frerich Raabe
1
@CMCDragonkai Vous avez absolument raison! Les types sont les «types de types». Une approche courante pour joindre les concepts de types et de valeurs est appelée typage dépendant . Idris est un langage typé dépendant inspiré de Haskell. Avec les bonnes extensions GHC, vous pouvez également vous rapprocher un peu de la saisie dépendante dans Haskell. (Certaines personnes ont plaisanté en disant que "la recherche Haskell consiste à déterminer à quel point nous pouvons nous rapprocher des types dépendants sans avoir de types dépendants.")
kqr
1
@CMCDragonkai Il n'est en fait pas possible d'avoir une déclaration de données vide dans Haskell standard. Mais il existe une extension GHC ( -XEmptyDataDecls) qui vous permet de le faire. Comme, comme vous le dites, il n'y a pas de valeurs avec ce type, une fonction f :: Int -> Zpeut par exemple ne jamais retourner (car que retournerait-elle?) Elles peuvent cependant être utiles lorsque vous voulez des types mais que vous ne vous souciez pas vraiment des valeurs .
kqr
1
Ce n'est vraiment pas possible? Je viens d'essayer dans GHC, et il l'a exécuté sans erreur. Je n'ai pas eu à charger d'extensions GHC, juste du GHC vanilla. Je pourrais alors écrire :k Zet ça m'a juste donné une étoile.
CMCDragonkai
42

Haskell a des types de données algébriques , que très peu d'autres langages ont. C'est peut-être ce qui vous déroute.

Dans d'autres langues, vous pouvez généralement créer un "enregistrement", une "structure" ou similaire, qui contient un tas de champs nommés contenant différents types de données. Vous pouvez aussi parfois faire une "énumération", qui a un (petit) ensemble de valeurs fixes possibles (par exemple, votre Red, Greenet Blue).

Dans Haskell, vous pouvez combiner les deux en même temps. Bizarre, mais vrai!

Pourquoi est-il appelé «algébrique»? Eh bien, les nerds parlent de «types de somme» et de «types de produits». Par exemple:

data Eg1 = One Int | Two String

Une Eg1valeur est essentiellement soit un nombre entier ou une chaîne. Ainsi, l'ensemble de toutes les Eg1valeurs possibles est la «somme» de l'ensemble de toutes les valeurs entières possibles et de toutes les valeurs de chaîne possibles. Ainsi, les nerds se réfèrent à Eg1un "type de somme". D'autre part:

data Eg2 = Pair Int String

Chaque Eg2valeur se compose à la fois d' un entier et d'une chaîne. Ainsi, l'ensemble de toutes les Eg2valeurs possibles est le produit cartésien de l'ensemble de tous les entiers et de l'ensemble de toutes les chaînes. Les deux ensembles sont "multipliés" ensemble, il s'agit donc d'un "type de produit".

Les types algébriques de Haskell sont des types de somme de types de produits . Vous donnez à un constructeur plusieurs champs pour créer un type de produit, et vous avez plusieurs constructeurs pour faire une somme (de produits).

Pour illustrer pourquoi cela pourrait être utile, supposons que vous ayez quelque chose qui génère des données au format XML ou JSON, et qu'il prend un enregistrement de configuration - mais évidemment, les paramètres de configuration pour XML et pour JSON sont totalement différents. Vous pourriez donc faire quelque chose comme ceci:

data Config = XML_Config {...} | JSON_Config {...}

(Avec quelques champs appropriés, évidemment.) Vous ne pouvez pas faire de choses comme ça dans les langages de programmation normaux, c'est pourquoi la plupart des gens n'y sont pas habitués.

MathématiqueOrchidée
la source
4
génial! juste une chose, "ils peuvent ... être construits dans presque n'importe quelle langue", dit Wikipedia . :) Dans par exemple C / ++, c'est unions, avec une discipline de tag. :)
Will Ness
5
Ouais, mais chaque fois que je mentionne union, les gens me regardent comme "qui diable utilise jamais ça ??" ;-)
MathematicalOrchid
1
J'en ai vu beaucoup unionutilisé dans ma carrière C. Veuillez ne pas le rendre inutile car ce n'est pas le cas.
truthadjustr
26

Commencez par le cas le plus simple:

data Color = Blue | Green | Red

Ceci définit un "constructeur de type" Colorqui ne prend aucun argument - et il a trois "constructeurs de données" Blue, Greenet Red. Aucun des constructeurs de données ne prend d'argument. Cela signifie que il y a trois de type Color: Blue, Greenet Red.

Un constructeur de données est utilisé lorsque vous devez créer une valeur quelconque. Comme:

myFavoriteColor :: Color
myFavoriteColor = Green

crée une valeur myFavoriteColorà l'aide du Greenconstructeur de données - et myFavoriteColorsera de type Colorpuisque c'est le type de valeurs produites par le constructeur de données.

Un constructeur de type est utilisé lorsque vous devez créer un type quelconque. C'est généralement le cas lors de l'écriture de signatures:

isFavoriteColor :: Color -> Bool

Dans ce cas, vous appelez le Colorconstructeur de type (qui ne prend aucun argument).

Encore avec moi?

Maintenant, imaginez que vous vouliez non seulement créer des valeurs rouge / vert / bleu, mais que vous vouliez également spécifier une "intensité". Comme, une valeur comprise entre 0 et 256. Vous pouvez le faire en ajoutant un argument à chacun des constructeurs de données, de sorte que vous vous retrouvez avec:

data Color = Blue Int | Green Int | Red Int

Maintenant, chacun des trois constructeurs de données prend un argument de type Int. Le constructeur de type ( Color) ne prend toujours aucun argument. Donc, ma couleur préférée étant un vert foncé, je pourrais écrire

    myFavoriteColor :: Color
    myFavoriteColor = Green 50

Et encore une fois, il appelle le Greenconstructeur de données et j'obtiens une valeur de type Color.

Imaginez si vous ne voulez pas dicter la manière dont les gens expriment l'intensité d'une couleur. Certains voudront peut-être une valeur numérique comme nous venons de le faire. D'autres peuvent convenir avec juste un booléen indiquant "brillant" ou "pas si brillant". La solution à cela est de ne pas coder Inten dur dans les constructeurs de données, mais plutôt d'utiliser une variable de type:

data Color a = Blue a | Green a | Red a

Maintenant, notre constructeur de type prend un argument (un autre type que nous venons d'appeler a!) Et tous les constructeurs de données prendront un argument (une valeur!) De ce type a. Alors tu pourrais avoir

myFavoriteColor :: Color Bool
myFavoriteColor = Green False

ou

myFavoriteColor :: Color Int
myFavoriteColor = Green 50

Remarquez comment nous appelons le Colorconstructeur de type avec un argument (un autre type) pour obtenir le type "effectif" qui sera retourné par les constructeurs de données. Cela touche au concept des types que vous voudrez peut-être lire autour d'une tasse de café ou deux.

Nous avons maintenant compris ce que sont les constructeurs de données et les constructeurs de types, et comment les constructeurs de données peuvent prendre d'autres valeurs comme arguments et les constructeurs de types peuvent prendre d'autres types comme arguments. HTH.

Frerich Raabe
la source
Je ne suis pas sûr d'être ami avec votre idée d'un constructeur de données nulles. Je sais que c'est une façon courante de parler des constantes dans Haskell, mais cela n'a-t-il pas été prouvé incorrect à plusieurs reprises?
kqr
@kqr: Un constructeur de données peut être nul, mais alors ce n'est plus une fonction. Une fonction est quelque chose qui prend un argument et donne une valeur, c'est-à-dire quelque chose avec ->dans la signature.
Frerich Raabe
Une valeur peut-elle pointer vers plusieurs types? Ou chaque valeur est-elle associée à un seul type et c'est tout?
CMCDragonkai
1
@jrg Il y a un certain chevauchement, mais ce n'est pas spécifiquement à cause des constructeurs de type mais à cause des variables de type, par exemple ain data Color a = Red a. aest un espace réservé pour un type arbitraire. Vous pouvez avoir la même chose dans les fonctions simples, par exemple, une fonction de type (a, b) -> aprend un tuple de deux valeurs (de types aet b) et donne la première valeur. C'est une fonction «générique» en ce qu'elle ne dicte pas le type des éléments de tuple - elle spécifie seulement que la fonction renvoie une valeur du même type que le premier élément de tuple.
Frerich Raabe
1
+1 Now, our type constructor takes one argument (another type which we just call a!) and all of the data constructors will take one argument (a value!) of that type a.Ceci est très utile.
Jonas
5

Comme d'autres l'ont souligné, le polymorphisme n'est pas si terrible que cela utile ici. Regardons un autre exemple que vous connaissez probablement déjà:

Maybe a = Just a | Nothing

Ce type a deux constructeurs de données. Nothingest un peu ennuyeux, il ne contient aucune donnée utile. D'autre part, il Justcontient une valeur de a- quel que soit le type a. Écrivons une fonction qui utilise ce type, par exemple obtenir la tête d'une Intliste, s'il y en a (j'espère que vous êtes d'accord que c'est plus utile que de lancer une erreur):

maybeHead :: [Int] -> Maybe Int
maybeHead [] = Nothing
maybeHead (x:_) = Just x

> maybeHead [1,2,3]    -- Just 1
> maybeHead []         -- None

Donc, dans ce cas, ac'est un Int, mais cela fonctionnerait aussi bien pour tout autre type. En fait, vous pouvez faire fonctionner notre fonction pour chaque type de liste (même sans changer l'implémentation):

maybeHead :: [t] -> Maybe t
maybeHead [] = Nothing
maybeHead (x:_) = Just x

D'autre part, vous pouvez écrire des fonctions qui n'acceptent qu'un certain type de Maybe, par exemple

doubleMaybe :: Maybe Int -> Maybe Int
doubleMaybe Just x = Just (2*x)
doubleMaybe Nothing= Nothing

Bref, avec le polymorphisme, vous donnez à votre propre type la flexibilité de travailler avec des valeurs de différents autres types.

Dans votre exemple, vous pouvez décider à un moment donné que ce Stringn'est pas suffisant pour identifier l'entreprise, mais elle doit avoir son propre type Company(qui contient des données supplémentaires telles que le pays, l'adresse, les comptes en arrière, etc.). Votre première implémentation de Cardevrait changer pour utiliser Companyau lieu de Stringpour sa première valeur. Votre deuxième implémentation est très bien, vous l'utilisez comme Car Company String Intet cela fonctionnerait comme avant (bien sûr, les fonctions accédant aux données de l'entreprise doivent être modifiées).

Landei
la source
Pouvez-vous utiliser des constructeurs de type dans le contexte de données d'une autre déclaration de données? Quelque chose comme data Color = Blue ; data Bright = Color? Je l'ai essayé dans ghci, et il semble que la couleur dans le constructeur de type n'a rien à voir avec le constructeur de données Color dans la définition Bright. Il n'y a que 2 constructeurs Color, l'un qui est Data et l'autre Type.
CMCDragonkai
@CMCDragonkai Je ne pense pas que vous puissiez faire cela, et je ne suis même pas sûr de ce que vous voulez réaliser avec cela. Vous pouvez "envelopper" un type existant en utilisant dataou newtype(par exemple data Bright = Bright Color), ou vous pouvez utiliser typepour définir un synonyme (par exemple type Bright = Color).
Landei
5

Le second contient la notion de «polymorphisme».

Le a b cpeut être de n'importe quel type. Par exemple, apeut être un [String], bpeut être [Int] et cpeut être [Char].

Alors que le premier type est fixe: la société est un String, le modèle est un Stringet l'année est Int.

L'exemple de Car pourrait ne pas montrer l'importance de l'utilisation du polymorphisme. Mais imaginez que vos données soient du type liste. Une liste peut contenir String, Char, Int ...Dans ces situations, vous aurez besoin de la deuxième façon de définir vos données.

Quant à la troisième façon, je ne pense pas qu'elle doive s'inscrire dans le type précédent. C'est juste une autre façon de définir les données dans Haskell.

C'est mon humble avis en tant que débutant moi-même.

Btw: Assurez-vous que vous entraînez bien votre cerveau et que vous vous sentez à l'aise pour cela. C'est la clé pour comprendre Monad plus tard.

McBear Holden
la source
1

Il s'agit de types : dans le premier cas, vous définissez les types String(pour la société et le modèle) et Intpour l'année. Dans le second cas, vous êtes plus générique. a, bet cpeuvent être du même type que dans le premier exemple, ou quelque chose de complètement différent. Par exemple, il peut être utile de donner l'année sous forme de chaîne au lieu d'un entier. Et si vous le souhaitez, vous pouvez même utiliser votre Colortype.

Matthias
la source