web-dev-qa-db-fra.com

Qu'est-ce que () à Haskell, exactement?

Je lis Learn You a Haskell, et dans les chapitres de la monade, il me semble que () est traité comme une sorte de "null" pour chaque type. Lorsque je vérifie le type de () dans GHCi, je reçois

>> :t ()
() :: ()

ce qui est une déclaration extrêmement déroutante. Il paraît que () est un type à lui tout seul. Je suis confus quant à la façon dont il s'intègre dans la langue, et comment il semble être capable de représenter n'importe quel type.

62
MYV

tl; dr() n'ajoute pas de valeur "null" à chaque type, enfer non; () Est une valeur "terne" dans un type qui lui est propre: ().

Permettez-moi de prendre un peu de recul par rapport à la question et d'aborder une source commune de confusion. Un élément clé à absorber lors de l'apprentissage de Haskell est la distinction entre son langage d'expression et son type langue. Vous savez probablement que les deux sont séparés. Mais cela permet d'utiliser le même symbole dans les deux, et c'est ce qui se passe ici. Il existe des indices textuels simples pour vous dire dans quelle langue vous regardez. Vous n'avez pas besoin d'analyser toute la langue pour détecter ces signaux.

Le niveau supérieur d'un module Haskell vit, par défaut, dans le langage d'expression. Vous définissez des fonctions en écrivant des équations entre les expressions. Mais quand vous voyez foo :: bar dans le langage d'expression, il signifie que foo est une expression et bar est son type. Ainsi, lorsque vous lisez () :: (), Vous voyez une déclaration qui relie le () Dans le langage d'expression avec le () Dans le langage de type. Les deux symboles () Signifient des choses différentes, car ils ne sont pas dans la même langue. Cette répétition provoque souvent de la confusion pour les débutants, jusqu'à ce que la séparation du langage expression/type s'installe dans leur subconscient, auquel cas elle devient utilement mnémonique.

Le mot clé data introduit une nouvelle déclaration de type de données, impliquant un mélange soigneux des langages d'expression et de type, car il indique d'abord ce qu'est le nouveau type, et ensuite quelles sont ses valeurs.

Les données TyCon tyvar ... tyvar = Type ValCon1 ... type | ... | Type ValConn ... type

Dans une telle déclaration, le constructeur de type TyCon est ajouté au langage de type et le ValCon des constructeurs de valeurs sont ajoutés au langage d'expression (et à son sous-langage de modèle). Dans une déclaration data, les éléments qui se trouvent dans des emplacements d'argument pour les ValCon s vous indiquent les types donnés aux arguments lorsque cela ValCon est utilisé dans les expressions. Par exemple,

data Tree a = Leaf | Node (Tree a) a (Tree a)

déclare un constructeur de type Tree pour les types d'arbres binaires stockant des éléments aux nœuds, dont les valeurs sont données par les constructeurs de valeur Leaf et Node. J'aime colorer les constructeurs de type (Tree) en bleu et les constructeurs de valeur (Leaf, Node) en rouge. Il ne doit pas y avoir de bleu dans les expressions et (sauf si vous utilisez des fonctionnalités avancées) pas de rouge dans les types. Le type intégré Bool pourrait être déclaré,

data Bool = True | False

en ajoutant le bleu Bool au langage de type et le rouge True et False au langage d'expression. Malheureusement, mon markdown-fu est insuffisant pour ajouter des couleurs à ce message, vous n'aurez donc qu'à apprendre à ajouter les couleurs dans votre tête.

Le type "unité" utilise () Comme symbole spécial, mais il fonctionne comme s'il était déclaré

data () = ()  -- the left () is blue; the right () is red

ce qui signifie qu'un () théoriquement bleu est un constructeur de type dans le langage de type, mais qu'un () théoriquement rouge est un constructeur de valeur dans le langage d'expression, et en fait () :: (). [Ce n'est pas le seul exemple d'un tel jeu de mots. Les types de tuples plus gros suivent le même modèle: la syntaxe des paires est comme si elle était donnée par

data (a, b) = (a, b)

ajouter (,) aux langages de type et d'expression. Mais je m'égare.

Ainsi, le type (), Souvent prononcé "Unit", est un type contenant une valeur digne d'être mentionnée: cette valeur est écrite () Mais dans le langage d'expression, et est parfois prononcée "void". Un type avec une seule valeur n'est pas très intéressant. Une valeur de type () Apporte zéro bit d'information: vous savez déjà ce que cela doit être. Ainsi, bien qu'il n'y ait rien de spécial dans le type () Pour indiquer les effets secondaires, il apparaît souvent comme le composant valeur d'un type monadique. Les opérations monadiques ont tendance à avoir des types qui ressemblent à

val-in-type-1 -> ... -> val-in-type-n -> effet-monade val-out-type

où le type de retour est une application de type: la fonction vous indique quels effets sont possibles et l'argument vous indique quelle sorte de valeur est produite par l'opération. Par exemple

put :: s -> State s ()

qui est lu (parce que l'application associe à gauche ["comme nous l'avons tous fait dans les années soixante", Roger Hindley]) comme

put :: s -> (State s) ()

a un type d'entrée de valeur s, la monade d'effet State s et le type de sortie de valeur (). Lorsque vous voyez () Comme type de sortie de valeur, cela signifie simplement que "cette opération est utilisée uniquement pour son effet ; la valeur fournie est inintéressant". De même

putStr :: String -> IO ()

fournit une chaîne à stdout mais ne renvoie rien d'excitant.

Le type () Est également utile comme type d'élément pour les structures de type conteneur, où il indique que les données consistent simplement en une forme , sans charge utile intéressante. Par exemple, si Tree est déclaré comme ci-dessus, alors Tree () est le type de formes d'arbre binaire, ne stockant rien d'intéressant aux nœuds. De même [()] est le type de listes d'éléments ternes, et s'il n'y a rien d'intéressant dans les éléments d'une liste, alors la seule information qu'elle apporte est sa longueur.

Pour résumer, () Est un type. Sa seule valeur, (), Se trouve avoir le même nom, mais c'est correct car les langages de type et d'expression sont séparés. Il est utile d'avoir un type représentant "aucune information" car, dans le contexte (par exemple, d'une monade ou d'un conteneur), il vous indique que seul le contexte est intéressant.

131
pigworker

Le type () Peut être considéré comme un tuple à zéro élément. C'est un type qui ne peut avoir qu'une seule valeur, et donc il est utilisé là où vous devez avoir un type, mais vous n'avez en fait pas besoin de transmettre d'informations. Voici quelques utilisations pour cela.

Les choses monadiques comme IO et State ont une valeur de retour, ainsi que des effets secondaires. Parfois, le seul point de l'opération est d'effectuer un effet secondaire, comme écrire sur l'écran ou stocker un état. Pour écrire sur l'écran, putStrLn doit avoir le type String -> IO ? - IO doit toujours avoir un type de retour, mais ici il n'y a rien d'utile à retourner. Alors, quel type devons-nous retourner? On pourrait dire Int et toujours retourner 0, mais c'est trompeur. Nous retournons donc (), Le type qui n'a qu'une seule valeur (et donc aucune information utile), pour indiquer qu'il n'y a rien d'utile à revenir.

Il est parfois utile d'avoir un type qui ne peut avoir aucune valeur utile. Considérez si vous avez implémenté un type Map k v Qui mappe les clés de type k aux valeurs de type v. Ensuite, vous voulez implémenter un Set, qui est vraiment similaire à une carte, sauf que vous n'avez pas besoin de la partie valeur, juste des clés. Dans un langage comme Java vous pouvez utiliser des booléens comme type de valeur fictive, mais vraiment vous voulez juste un type qui n'a pas de valeurs utiles. Vous pouvez donc dire type Set k = Map k ()

Il convient de noter que () N'est pas particulièrement magique. Si vous le souhaitez, vous pouvez le stocker dans une variable et faire une correspondance de modèle (bien qu'il n'y ait pas grand-chose):

main = do
  x <- putStrLn "Hello"
  case x of
    () -> putStrLn "The only value..."
31
Neil Brown

Il est appelé le type Unit, généralement utilisé pour représenter les effets secondaires. Vous pouvez l'imaginer vaguement comme Void en Java. En savoir plus ici et ici etc. Ce qui peut prêter à confusion, c'est que () représente syntaxiquement à la fois le type et sa seule valeur littérale. Notez également qu'il n'est pas similaire à null dans Java ce qui signifie une référence non définie - () est simplement un tuple de taille 0.

12
thSoft

J'aime vraiment penser à () Par analogie avec les tuples.

(Int, Char) Est le type de toutes les paires d'un Int et d'un Char, donc ses valeurs sont toutes les valeurs possibles de Int croisées avec toutes les valeurs possibles de Char. (Int, Char, String) Est pareillement le type de tous les triplets d'un Int, un Char et un String.

Il est facile de voir comment continuer à étendre ce modèle vers le haut, mais qu'en est-il vers le bas?

(Int) Serait le type "1-Tuple", composé de toutes les valeurs possibles de Int. Mais cela serait analysé par Haskell comme mettant simplement des parenthèses autour de Int, et n'étant donc que le type Int. Et les valeurs de ce type seraient (1), (2), (3), Etc., qui seraient également simplement analysées en tant que valeurs Int ordinaires entre parenthèses. Mais si vous y réfléchissez, un "1-Tuple" est exactement la même chose qu'une simple valeur, il n'est donc pas nécessaire de les faire réellement exister.

Descendre un peu plus loin jusqu'à zéro-tuples nous donne (), Qui devrait être toutes les combinaisons possibles de valeurs dans une liste vide de types. Eh bien, il y a exactement une façon de le faire, qui est de ne contenir aucune autre valeur, donc il ne devrait y avoir qu'une seule valeur dans le type (). Et par analogie avec la syntaxe des valeurs de Tuple, nous pouvons écrire cette valeur sous la forme (), Ce qui ressemble certainement à un Tuple ne contenant aucune valeur.

C'est exactement comme ça que ça fonctionne. Il n'y a pas de magie, et ce type () Et sa valeur () Ne sont en aucun cas traités spécialement par le langage.

() N'est en fait pas traité comme "une valeur nulle pour tout type" dans les exemples de monades du livre LYAH. Chaque fois que le type () Est utilisé, la valeur uniquement qui peut être renvoyée est (). Il est donc utilisé comme type pour explicitement dire qu'il ne peut pas être toute autre valeur de retour. De même, lorsqu'un autre type est censé être renvoyé, vous ne pouvez pas retourner ().

La chose à garder à l'esprit est que lorsqu'un tas de calculs monadiques sont composés avec des blocs do ou des opérateurs comme >>=, >>, Etc., ils construiront un valeur de type m a pour certaines monades m. Ce choix de m doit rester le même dans toutes les parties du composant (il n'y a aucun moyen de composer un Maybe Int Avec un IO Int De cette façon), mais le a peut et très souvent est différent à chaque étape.

Donc, quand quelqu'un colle une IO () au milieu d'un calcul IO String, Cela n'utilise pas le () Comme un null dans le type String, c'est simplement en utilisant une IO () sur la façon de construire un IO String, de la même manière que vous pourriez utilisez un Int pour construire un String.

7
Ben

La confusion vient d'autres langages de programmation: "void" signifie dans la plupart des langages impératifs qu'il n'y a pas de structure en mémoire stockant une valeur. Cela semble incohérent car "booléen" a 2 valeurs au lieu de 2 bits, tandis que "void" n'a pas de bits au lieu de pas de valeurs, mais là, il s'agit de quoi une fonction revient dans un sens pratique. Pour être exact: sa valeur unique ne consomme aucun morceau de stockage.

Ignorons la valeur bottom (écrite _|_) pour un moment...

() est appelé Unit, écrit comme un null-Tuple. Il n'a qu'une seule valeur. Et il ne s'appelle pas Void, car Void n'a même pas de valeur, donc ne peut être retourné par aucune fonction.


Observez ceci: Bool a 2 valeurs (True et False), () a une valeur (()) et Void n'a pas de valeur (elle n'existe pas). Ils sont comme des ensembles avec deux/un/aucun élément. Le moins de mémoire dont ils ont besoin pour stocker leur valeur est respectivement de 1 bit/pas de bit/impossible. Ce qui signifie qu'une fonction qui renvoie un () peut revenir avec une valeur de résultat (la plus évidente) qui peut vous être inutile. Void d'autre part impliquerait que cette fonction ne retournera jamais et ne vous donnera jamais de résultat, car il n'existerait aucun résultat.

Si vous voulez donner un nom à "cette valeur", qu'une fonction retourne qui ne retourne jamais (oui, cela ressemble à une folle conversation), alors appelez-la bottom ("_|_ ", écrit comme un T inversé. Il peut représenter une exception ou une boucle à l'infini ou un blocage ou" juste attendre plus longtemps ". (Certaines fonctions ne renvoient alors que bottom, si l'un de leurs paramètres est bottom.)

Lorsque vous créez le produit cartésien/un tuple de ces types, vous observerez le même comportement: (Bool,Bool,Bool,(),()) a 2 · 2 · 2 · 1 · 1 = 6 valeurs différentes. (Bool,Bool,Bool,(),Void) est comme l'ensemble {t, f} × {t, f} × {t, f} × {u} × {} qui a 2 · 2 · 2 · 1 · 0 = 0 éléments, sauf si vous comptez _|_ comme valeur.

6
comonad

Encore un autre angle:

() est le nom d'un ensemble qui contient un seul élément appelé ().

Il est en effet un peu déroutant que le nom de l'ensemble et l'élément qu'il contient se trouvent être les mêmes dans ce cas.

Rappelez-vous: dans Haskell, un type est un ensemble qui a ses valeurs possibles comme éléments.

5
jhegedus