Que signifie la syntaxe «Just» dans Haskell?

118

J'ai parcouru Internet pour une explication réelle de ce que fait ce mot-clé. Chaque tutoriel Haskell que j'ai regardé commence juste à l'utiliser de manière aléatoire et n'explique jamais ce qu'il fait (et j'en ai regardé beaucoup).

Voici un morceau de code de base de Real World Haskell qui utilise Just. Je comprends ce que fait le code, mais je ne comprends pas quel est le but ou la fonction Just.

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

D'après ce que j'ai observé, c'est lié à la Maybefrappe, mais c'est à peu près tout ce que j'ai réussi à apprendre.

Une bonne explication de ce que Justsignifie serait très appréciée.

reem
la source

Réponses:

211

C'est en fait juste un constructeur de données normal qui se trouve être défini dans Prelude , qui est la bibliothèque standard qui est importée automatiquement dans chaque module.

Ce qui est peut-être, structurellement

La définition ressemble à ceci:

data Maybe a = Just a
             | Nothing

Cette déclaration définit un type, Maybe aqui est paramétré par une variable de type a, ce qui signifie simplement que vous pouvez l'utiliser avec n'importe quel type à la place de a.

Construire et détruire

Le type a deux constructeurs Just aet Nothing. Lorsqu'un type a plusieurs constructeurs, cela signifie qu'une valeur du type doit avoir été construite avec un seul des constructeurs possibles. Pour ce type, une valeur a été construite via JustouNothing , il n'y a pas d'autres possibilités (sans erreur).

Puisque Nothingn'a pas de type de paramètre, lorsqu'il est utilisé comme constructeur, il nomme une valeur constante qui est membre de type Maybe apour tous les types a. Mais le Justconstructeur a un paramètre de type, ce qui signifie que lorsqu'il est utilisé comme constructeur, il agit comme une fonction de type aà Maybe a, c'est- à -dire qu'il a le typea -> Maybe a

Ainsi, les constructeurs d'un type construisent une valeur de ce type; L'autre aspect des choses, c'est quand vous souhaitez utiliser cette valeur, et c'est là que la correspondance de motifs entre en jeu. Contrairement aux fonctions, les constructeurs peuvent être utilisés dans les expressions de liaison de modèles, et c'est ainsi que vous pouvez effectuer une analyse de cas des valeurs appartenant à des types avec plusieurs constructeurs.

Afin d'utiliser une Maybe avaleur dans une correspondance de modèle, vous devez fournir un modèle pour chaque constructeur, comme ceci:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

Dans ce cas, le premier modèle correspondrait si la valeur était Nothing, et le second correspondrait si la valeur a été construite avec Just. Si le second correspond, il lie également le nom valau paramètre qui a été passé au Justconstructeur lors de la construction de la valeur à laquelle vous correspondez.

Que signifie peut-être

Peut-être que vous saviez déjà comment cela fonctionnait; il n'y a pas vraiment de magie dans les Maybevaleurs, c'est juste un type de données algébrique Haskell normal (ADT). Mais il est assez utilisé car il "soulève" ou étend effectivement un type, comme celui Integerde votre exemple, dans un nouveau contexte dans lequel il a une valeur supplémentaire ( Nothing) qui représente un manque de valeur! Le système de types exige alors que vous vérifiiez cette valeur supplémentaire avant de vous permettre d'accéder à ce Integerqui pourrait être là. Cela évite un nombre remarquable de bugs.

Aujourd'hui, de nombreux langages gèrent ce type de valeur «sans valeur» via des références NULL. Tony Hoare, un éminent informaticien (il a inventé Quicksort et est un lauréat du prix Turing), reconnaît cela comme son "erreur d'un milliard de dollars" . Le type Maybe n'est pas le seul moyen de résoudre ce problème, mais il s'est avéré être un moyen efficace de le faire.

Peut-être en tant que foncteur

L'idée de transformer un type en un autre de sorte que les opérations sur l'ancien type puissent également être transformées pour travailler sur le nouveau type est le concept derrière la classe de type Haskell appelée Functor, qui Maybe aa une instance utile de.

Functorfournit une méthode appelée fmap, qui mappe les fonctions qui s'étendent sur des valeurs du type de base (comme Integer) aux fonctions qui s'étendent sur des valeurs du type levé (comme Maybe Integer). Une fonction transformée avec fmappour travailler sur une Maybevaleur fonctionne comme ceci:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

Donc, si vous avez une Maybe Integervaleur m_xet une Int -> Intfonction f, vous pouvez fmap f m_xappliquer la fonction fdirectement au Maybe Integersans vous soucier si elle a réellement une valeur ou non. En fait, vous pouvez appliquer toute une chaîne de Integer -> Integerfonctions levées aux Maybe Integervaleurs et ne vous soucier de vérifier explicitement Nothingqu'une seule fois lorsque vous avez terminé.

Peut-être en tant que Monade

Je ne sais pas à quel point vous connaissez encore le concept de a Monad, mais vous l'avez au moins utilisé IO aauparavant, et la signature de type IO aressemble remarquablement à Maybe a. Bien qu'il IOsoit spécial en ce qu'il ne vous expose pas ses constructeurs et ne peut donc être "exécuté" que par le système d'exécution Haskell, il est toujours aussi Functorun Monad. En fait, il y a un sens important dans lequel a Monadest juste un type spécial Functoravec quelques fonctionnalités supplémentaires, mais ce n'est pas le lieu d'entrer dans cela.

Quoi qu'il en soit, les monades aiment les IOtypes de carte vers de nouveaux types qui représentent des «calculs qui donnent des valeurs» et vous pouvez transformer des fonctions en Monadtypes via une fmapfonction très similaire appelée liftMqui transforme une fonction régulière en un «calcul qui aboutit à la valeur obtenue en évaluant le fonction."

Vous avez probablement deviné (si vous avez lu jusqu'ici) que Maybec'est aussi un fichier Monad. Il représente "des calculs qui pourraient ne pas renvoyer une valeur". Tout comme avec l' fmapexemple, cela vous permet de faire tout un tas de calculs sans avoir à vérifier explicitement les erreurs après chaque étape. Et en fait, la façon dont l' Monadinstance est construite, un calcul sur des Maybevaleurs s'arrête dès que a Nothingest rencontré, donc c'est un peu comme un abandon immédiat ou un retour sans valeur au milieu d'un calcul.

Tu aurais peut-être écrit

Comme je l'ai déjà dit, il n'y a rien d'inhérent au Maybetype qui est intégré à la syntaxe du langage ou au système d'exécution. Si Haskell ne l'a pas fourni par défaut, vous pouvez fournir toutes ses fonctionnalités vous-même! En fait, vous pouvez le réécrire vous-même de toute façon, avec des noms différents, et obtenir la même fonctionnalité.

J'espère que vous comprenez le Maybetype et ses constructeurs maintenant, mais s'il n'y a toujours rien de clair, faites le moi savoir!

Levi Pearson
la source
19
Quelle réponse exceptionnelle! Il convient de mentionner que Haskell utilise souvent Maybelà où d'autres langues utiliseraient nullou nil(avec des NullPointerExceptions méchants cachés dans tous les coins). Maintenant, d'autres langages commencent également à utiliser cette construction: Scala as Option, et même Java 8 aura le Optionaltype.
Landei
3
C'est une excellente explication. Bon nombre des explications que j'ai lues faisaient en quelque sorte allusion à l'idée que Just est un constructeur pour le type Maybe, mais aucune ne l'a jamais vraiment rendu explicite.
reem le
@Landei, merci pour la suggestion. J'ai fait une modification pour faire référence au danger des références nulles.
Levi Pearson
@Landei Le type d'option existe depuis ML dans les années 70, il est plus probable que Scala l'ait repris car Scala utilise la convention de dénomination de ML, Option avec des constructeurs certains et aucun.
stonemetal
Swift de @Landei Apple utilise également un peu les options
Jamin
37

La plupart des réponses actuelles sont des explications très techniques sur la façon dont les Justamis travaillent; J'ai pensé que je pourrais essayer d'expliquer à quoi ça sert.

De nombreux langages ont une valeur comme nullcelle-là qui peut être utilisée à la place d'une valeur réelle, du moins pour certains types. Cela a mis beaucoup de gens en colère et a été largement considéré comme une mauvaise décision. Pourtant, il est parfois utile d'avoir une valeur comme nullpour indiquer l'absence d'une chose.

Haskell résout ce problème en vous obligeant à marquer explicitement les endroits où vous pouvez avoir un Nothing(sa version de a null). Fondamentalement, si votre fonction renvoie normalement le type Foo, elle doit à la place renvoyer le type Maybe Foo. Si vous voulez indiquer qu'il n'y a pas de valeur, retournez Nothing. Si vous souhaitez renvoyer une valeur bar, vous devez à la place renvoyer Just bar.

Donc, fondamentalement, si vous ne pouvez pas avoir Nothing, vous n'en avez pas besoin Just. Si vous pouvez avoir Nothing, vous avez besoin Just.

Il n'y a rien de magique Maybe; il est construit sur le système de type Haskell. Cela signifie que vous pouvez utiliser toutes les astuces habituelles de correspondance de motifs Haskell .

Brent Royal-Gordon
la source
1
Très belle réponse à côté des autres réponses, mais je pense que cela bénéficierait toujours d'un exemple de code :)
PascalVKooten
13

Étant donné un type t, une valeur de Just test une valeur existante de type t, où Nothingreprésente un échec pour atteindre une valeur, ou un cas où avoir une valeur n'aurait pas de sens.

Dans votre exemple, avoir un solde négatif n'a pas de sens, et donc si une telle chose se produit, elle est remplacée par Nothing.

Pour un autre exemple, cela pourrait être utilisé dans la division, en définissant une fonction de division qui prend aet b, et retourne Just a/bsi best différent de zéro, et Nothingsinon. Il est souvent utilisé comme ceci, comme alternative pratique aux exceptions, ou comme votre exemple précédent, pour remplacer des valeurs qui n'ont pas de sens.

qaphla
la source
1
Alors disons dans le code ci-dessus que j'ai supprimé le Just, pourquoi cela ne fonctionnerait-il pas? Devez-vous avoir juste à chaque fois que vous voulez avoir un rien? Qu'arrive-t-il à une expression où une fonction à l'intérieur de cette expression renvoie Nothing?
reem le
7
Si vous supprimez le Just, votre code ne sera pas vérifié. La raison Justest de conserver les types appropriés. Il existe un type (une monade, en fait, mais il est plus facile de penser qu'il s'agit simplement d'un type) Maybe t, qui se compose d'éléments de la forme Just tet Nothing. Depuis Nothinga type Maybe t, une expression qui peut s'évaluer à l'une Nothingou l' autre des valeurs de type tn'est pas correctement typée. Si une fonction retourne Nothingdans certains cas, toute expression utilisant cette fonction doit avoir un moyen de vérifier cela ( isJustou une instruction case), afin de gérer tous les cas possibles.
qaphla
2
Donc, le Just est simplement là pour être cohérent dans le type Maybe parce qu'un t régulier n'est pas dans le type Maybe. Tout est beaucoup plus clair maintenant. Je vous remercie!
reem
3
@qaphla: Votre commentaire sur le fait qu'il s'agit d'une "monade, en fait, [...]" est trompeur. Maybe t est juste un type. Le fait qu'il existe une Monadinstance pour Maybene la transforme pas en quelque chose qui n'est pas un type.
Sarah
2

Une fonction totale a-> b peut trouver une valeur de type b pour chaque valeur possible de type a.

Chez Haskell, toutes les fonctions ne sont pas totales. Dans ce cas particulier, la fonction lendn'est pas totale - elle n'est pas définie pour le cas où le solde est inférieur à la réserve (bien qu'à mon goût, il serait plus logique de ne pas permettre à newBalance d'être inférieur à la réserve - en l'état, vous pouvez emprunter 101 à un solde de 100).

Autres conceptions qui traitent des fonctions non totales:

  • lever des exceptions lors de la vérification de la valeur d'entrée ne correspond pas à la plage
  • renvoie une valeur spéciale (type primitif): le choix favori est une valeur négative pour les fonctions entières destinées à renvoyer des nombres naturels (par exemple, String.indexOf - lorsqu'une sous-chaîne n'est pas trouvée, l'index renvoyé est généralement conçu pour être négatif)
  • retourne une valeur spéciale (pointeur): NULL ou une telle
  • revenir silencieusement sans rien faire: par exemple, lendpourrait être écrit pour retourner l'ancien solde, si la condition de prêt n'est pas remplie
  • renvoie une valeur spéciale: rien (ou enveloppe à gauche un objet de description d'erreur)

Ce sont des limitations de conception nécessaires dans les langages qui ne peuvent pas appliquer la totalité des fonctions (par exemple, Agda peut, mais cela conduit à d'autres complications, comme devenir incomplet).

Le problème avec le retour d'une valeur spéciale ou la levée d'exceptions est qu'il est facile pour l'appelant d'omettre la gestion d'une telle possibilité par erreur.

Le problème de la suppression silencieuse d'un échec est également évident: vous limitez ce que l'appelant peut faire avec la fonction. Par exemple, si l' lendancien solde est retourné, l'appelant n'a aucun moyen de savoir si le solde a changé. Cela peut ou non être un problème, selon le but recherché.

La solution de Haskell force l'appelant d'une fonction partielle à traiter le type comme Maybe a, ou à Either error acause du type de retour de la fonction.

De cette façon, lendtelle qu'elle est définie, est une fonction qui ne calcule pas toujours le nouvel équilibre - dans certaines circonstances, le nouvel équilibre n'est pas défini. Nous signalons cette circonstance à l'appelant en retournant la valeur spéciale Nothing ou en enveloppant le nouveau solde dans Just. L'appelant a désormais la liberté de choisir: soit gérer le défaut de prêt d'une manière spéciale, soit ignorer et utiliser l'ancien solde - par exemple maybe oldBalance id $ lend amount oldBalance,.

Sassa NF
la source
-1

La fonction if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a)doit avoir le même type de ifTrueet ifFalse.

Donc, lorsque nous écrivons then Nothing, nous devons utiliser le Maybe atype danselse f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type
esprit
la source
1
Je suis sûr que vous avez voulu dire quelque chose de vraiment profond ici
sehe
1
Haskell a l'inférence de type. Il n'est pas nécessaire d'indiquer le type de Nothinget Just newBalanceexplicitement.
droite le
Pour cette explication aux non-initiés, les types explicites clarifient le sens de l'utilisation de Just.
philip