J'apprends Haskell de learnyouahaskell.com . J'ai de la difficulté à 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 le premier consiste simplement à utiliser un constructeur (Car
) pour construire 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 j'ai compris, le troisième exemple (Color
) est un type pouvant avoir trois états: Blue
, Green
ou Red
. Mais cela entre en conflit avec ma compréhension des deux premiers exemples: est-ce que le type Car
ne peut être que dans un seul état, Car
, qui peut nécessiter différents paramètres pour être généré? Si oui, comment se situe le deuxième exemple?
Je cherche essentiellement une explication qui unifie les trois exemples/constructions de code ci-dessus.
Dans une déclaration data
, un constructeur de type est l'élément situé à gauche du signe égal. Les constructeur (s) de données sont les éléments situés à droite du signe égal. Vous utilisez des constructeurs de type où un type est attendu et des constructeurs de données où une valeur est attendue.
Pour simplifier les choses, nous pouvons commencer par un exemple de type représentant une couleur.
data Colour = Red | Green | Blue
Ici, nous avons trois constructeurs de données. Colour
est un type et Green
est un constructeur qui contient une valeur de type Colour
. De même, Red
et Blue
sont deux 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 RGB
n'est pas une valeur - c'est une fonction prenant trois Ints et retournant une valeur! RGB
a le type
RGB :: Int -> Int -> Int -> Colour
RGB
est un constructeur de données qui est une fonction prenant comme argument valeurs, puis utilise ceux-ci pour construire une nouvelle valeur. Si vous avez effectué une programmation orientée objet, vous devriez le reconnaître. Dans OOP, les constructeurs prennent également certaines valeurs en 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 Colour
en appliquant le constructeur de données. Un constructeur de données contient une valeur comme le ferait une variable ou prend d'autres valeurs en tant qu'argument et crée un nouveau valeur. Si vous avez déjà programmé, ce concept ne devrait pas vous être très étrange.
Si vous voulez construire un arbre binaire pour stocker String
s, imaginez-vous faire quelque chose comme:
data SBTree = Leaf String
| Branch String SBTree SBTree
Ce que nous voyons ici est un type SBTree
qui contient deux constructeurs de données. En d’autres termes, il existe deux fonctions (à savoir Leaf
et Branch
) qui construiront les valeurs du type SBTree
. Si vous ne connaissez pas le fonctionnement des arbres binaires, accrochez-vous. Vous n'avez pas besoin de savoir comment fonctionnent les arbres binaires, mais seulement que celui-ci stocke String
s d'une manière ou d'une autre.
Nous constatons également que les deux constructeurs de données prennent un argument String
- il s'agit de la chaîne qu'ils vont stocker dans l'arborescence.
Mais! Et si nous voulions aussi pouvoir stocker Bool
, nous devrions créer un nouvel arbre binaire. Cela pourrait ressembler à quelque chose comme ça:
data BBTree = Leaf Bool
| Branch Bool BBTree BBTree
SBTree
et BBTree
sont tous deux des constructeurs de types. Mais il y a un problème criant. Voyez-vous à quel point ils sont similaires? C'est un signe que vous voulez vraiment un paramètre quelque part.
Donc on peut faire ça:
data BTree a = Leaf a
| Branch a (BTree a) (BTree a)
Maintenant, nous introduisons un type variablea
en tant que paramètre du constructeur de type. Dans cette déclaration, BTree
est devenu 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]
etMaybe Bool
) qui est un type qui peut être assigné à une valeur dans votre programme et un fonction constructeur du type dont vous avez besoin pour alimenter un type afin de 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 il doit s'agir d'un "arbre binaire stockant quelque chose".
Si nous transmettons, disons, Bool
en tant qu'argument à BTree
, il retourne le type BTree Bool
, qui est un arbre binaire qui stocke Bool
s. Remplacez chaque occurrence de la variable de type a
par le type Bool
et vous pourrez voir par vous-même en quoi cela est vrai.
Si vous le souhaitez, vous pouvez afficher BTree
en tant que fonction avec le kind
BTree :: * -> *
Les genres ressemblent un peu aux types - le *
indique un type concret, nous disons donc BTree
est un type concret à un type concret.
Reculez un instant ici et notez les similitudes.
Un constructeur de données est une "fonction" qui prend 0 ou plus valeurs et vous redonne une nouvelle valeur.
Un type constructeur est une "fonction" qui prend 0 ou plus types et vous redonne un nouveau type.
Les constructeurs de données avec paramètres sont cool si nous voulons de légères variations dans nos valeurs - nous mettons ces variations dans les paramètres et laissons le responsable de la création de la valeur décider des arguments à utiliser. Dans le même sens, les constructeurs de type avec paramètres sont cool si nous voulons de légères variations dans nos types! Nous mettons ces variations en tant que paramètres et laissons le responsable du type décider des arguments à utiliser.
Comme la maison s'étend ici, nous pouvons considérer le Maybe a
type. Sa définition est
data Maybe a = Nothing
| Just a
Ici, Maybe
est un constructeur de type qui retourne un type concret. Just
est un constructeur de données qui renvoie une valeur. Nothing
est un constructeur de données contenant une valeur. Si nous regardons le type de Just
, nous voyons que
Just :: a -> Maybe a
En d'autres termes, Just
prend une valeur de type a
et renvoie une valeur de type Maybe a
. Si nous regardons le genre de Maybe
, nous voyons que
Maybe :: * -> *
En d'autres termes, Maybe
prend un type concret et renvoie un type concret.
Encore une fois! Différence entre un type concret et une fonction constructeur de type. Vous ne pouvez pas créer une liste de Maybe
s - si vous essayez d’exécuter
[] :: [Maybe]
vous aurez une erreur. Vous pouvez cependant créer une liste de Maybe Int
, ou Maybe a
. En effet, Maybe
est une fonction constructeur de type, mais une liste doit contenir les valeurs d'un type concret. Maybe Int
et Maybe a
sont des types concrets (ou, si vous le souhaitez, des appels à des fonctions de constructeur qui retournent des types concrets.)
Haskell a types de données algébriques, que très peu d’autres langues ont. C'est peut-être ce qui vous trouble.
Dans d'autres langues, vous pouvez généralement créer un "enregistrement", un "struct" ou similaire, comportant un ensemble de champs nommés contenant divers types de données. Vous pouvez aussi parfois faire une "énumération", qui a un (petit) ensemble de valeurs possibles fixes (par exemple, votre Red
, Green
et Blue
.).
En Haskell, vous pouvez combiner les deux à la fois. Bizarre, mais vrai!
Pourquoi s'appelle-t-il "algébrique"? Eh bien, les nerds parlent de "types de somme" et de "types de produit". Par exemple:
data Eg1 = One Int | Two String
Un Eg1
valeur est fondamentalement soit un entier ou une chaîne. Donc, l'ensemble de tous les possibles Eg1
values 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 font référence à Eg1
comme un "type somme". D'autre part:
data Eg2 = Pair Int String
Chaque Eg2
valeur consiste en les deux un entier et une chaîne. Donc, l'ensemble de tous les possibles Eg2
values est le produit cartésien de l’ensemble des entiers et de l’ensemble des 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 somme des 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).
Par exemple, supposons que quelque chose produise des données au format XML ou JSON, ce qui nécessite un enregistrement de configuration, mais les paramètres de configuration de XML et de JSON sont bien évidemment totalement différents. Donc vous peut-être faites quelque chose comme ceci:
data Config = XML_Config {...} | JSON_Config {...}
(Bien sûr, avec certains champs appropriés.) Vous ne pouvez pas faire ce genre de choses dans les langages de programmation normaux, ce qui explique pourquoi la plupart des gens ne sont pas habitués à cela.
Commencez par le cas le plus simple:
data Color = Blue | Green | Red
Ceci définit un "constructeur de type" Color
qui ne prend aucun argument - et il possède trois "constructeurs de données", Blue
, Green
et Red
. Aucun des constructeurs de données ne prend d'arguments. Cela signifie qu'il en existe trois de type Color
: Blue
, Green
et Red
.
Un constructeur de données est utilisé lorsque vous devez créer une valeur. Comme:
myFavoriteColor :: Color
myFavoriteColor = Green
crée une valeur myFavoriteColor
à l'aide du constructeur de données Green
- et myFavoriteColor
sera de type Color
puisque 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 . C'est généralement le cas lors de l'écriture de signatures:
isFavoriteColor :: Color -> Bool
Dans ce cas, vous appelez le constructeur de type Color
(qui ne prend aucun argument).
Encore avec moi?
Maintenant, imaginez que vous vouliez non seulement créer des valeurs rouge/vert/bleu, mais également spécifier une "intensité". Par exemple, une valeur comprise entre 0 et 256. Vous pouvez le faire en ajoutant un argument à chacun des constructeurs de données. Vous obtenez ainsi:
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 pas d'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 constructeur de données Green
et je reçois une valeur de type Color
.
Imaginez si vous ne voulez pas dicter la façon 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 être bien avec juste un booléen indiquant "brillant" ou "pas si brillant". La solution à ce problème consiste à ne pas coder en dur Int
dans les constructeurs de données, mais plutôt à 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 appelons simplement a
!) Et tous les constructeurs de données prendront un argument (une valeur!) De ce type a
. Donc vous pourriez avoir
myFavoriteColor :: Color Bool
myFavoriteColor = Green False
ou
myFavoriteColor :: Color Int
myFavoriteColor = Green 50
Remarquez comment nous appelons le constructeur de type Color
avec un argument (un autre type) pour obtenir le type "effectif" qui sera renvoyé par les constructeurs de données. Cela touche le concept de types que vous voudrez peut-être lire au-dessus d’une tasse de café ou de deux.
Maintenant, nous avons 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 en tant qu'arguments et les constructeurs de types peuvent prendre d'autres types en tant qu'arguments. HTH.
Comme d'autres l'ont souligné, le polymorphisme n'est pas très 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. Nothing
est quelque peu ennuyeux, il ne contient aucune donnée utile. Par contre, Just
contient une valeur de a
- quel que soit le type a
que puisse avoir. Écrivons une fonction qui utilise ce type, par exemple. obtenir la tête d'une liste Int
, s'il y en a une (j'espère que vous êtes d'accord, c'est plus utile que de jeter une erreur):
maybeHead :: [Int] -> Maybe Int
maybeHead [] = Nothing
maybeHead (x:_) = Just x
> maybeHead [1,2,3] -- Just 1
> maybeHead [] -- None
Donc, dans ce cas, a
est un Int
, mais cela fonctionnerait également pour tout autre type. En fait, vous pouvez utiliser 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é nécessaire pour travailler avec des valeurs de différents autres types.
Dans votre exemple, vous pouvez décider à un moment donné que String
ne suffit pas pour identifier la société, mais doit avoir son propre type Company
(qui contient des données supplémentaires telles que pays, adresse, etc.). les comptes de retour, etc.). Votre première implémentation de Car
devrait changer pour utiliser Company
au lieu de String
pour sa première valeur. Votre deuxième implémentation est très bien, vous l’utilisez comme Car Company String Int
et cela fonctionnerait comme avant (bien sûr, les fonctions accédant aux données de l’entreprise doivent être changées).
La seconde contient la notion de "polymorphisme".
Le a b c
peut être de tout type. Par exemple, a
peut être un [String]
, b
peut être [Int]
et c
peuvent être [Char]
.
Alors que le type du premier est fixe: société est un String
, modèle est un String
et l'année est Int
.
L’exemple Car pourrait ne pas montrer l’importance de l’utilisation du polymorphisme. Mais imaginez que vos données sont 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.
Pour ce qui est de 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 des données en Haskell.
Ceci est mon humble avis en tant que débutant moi-même.
Btw: Assurez-vous que vous entraînez bien votre cerveau et vous sentez à l'aise pour cela. C'est la clé pour comprendre Monad plus tard.
Il s’agit de types : dans le premier cas, vous définissez les types String
(pour la société et le modèle) et Int
pour l'année. Dans le second cas, vous êtes plus générique. a
, b
et c
peuvent être du même type que dans le premier exemple, o quelque chose de complètement différent. Par exemple, il peut être utile d'indiquer l'année sous forme de chaîne au lieu d'un entier. Et si vous le souhaitez, vous pouvez même utiliser votre type Color
.