web-dev-qa-db-fra.com

Type Haskell vs Constructeur de données

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.

111
Aristides

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.

Constructeurs de données

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.

Entracte

Si vous voulez construire un arbre binaire pour stocker Strings, 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 Strings 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

Constructeurs de types

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] et Maybe 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 Bools. 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.

Emballer

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.

Une étude de cas

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

209
kqr

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.

40
MathematicalOrchid

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.

24
Frerich Raabe

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

5
Landei

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.

5
McBear Holden

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.

1
Matthias