web-dev-qa-db-fra.com

Que signifie la syntaxe "Just" dans Haskell?

J'ai parcouru Internet pour une explication de ce que fait ce mot clé. Tous les tutoriels que j'ai consultés sur Haskell commencent simplement à l’utiliser de manière aléatoire et n’expliquent jamais ce qu’il fait (et j’en ai examiné beaucoup).

Voici un morceau de code de base de Real World Haskell qui utilise Just. Je comprends ce que le code fait, mais je ne comprends pas quel est le but ou la fonction de 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é, il s'agit de Maybe dactylographie, mais c'est à peu près tout ce que j'ai réussi à apprendre.

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

108
reem

Il s’agit en fait d’un constructeur de données normal défini dans le prélude Prelude , qui est la bibliothèque standard 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 a, 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 a Et 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 Just ou Nothing, il n'y a pas d'autre possibilité (sans erreur).

Puisque Nothing n'a pas de type de paramètre, lorsqu'il est utilisé en tant que constructeur, il nomme une valeur constante membre du type Maybe a Pour tous les types a. Mais le constructeur Just a effectivement un paramètre de type, ce qui signifie que, utilisé comme constructeur, il agit comme une fonction du type a à Maybe a, C'est-à-dire qu'il a le type a -> Maybe a

Ainsi, les constructeurs d'un type construisent une valeur de ce type; De plus, lorsque vous souhaitez utiliser cette valeur, c’est là que la correspondance des motifs entre en jeu. Contrairement aux fonctions, les constructeurs peuvent être utilisés dans les expressions de liaison de modèle, et c’est ainsi que vous pouvez analyser les cas de valeurs appartenant à des types avec plus qu'un constructeur.

Pour utiliser une valeur Maybe a Dans une correspondance de modèle, vous devez fournir un modèle pour chaque constructeur, comme suit:

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

Dans cette expression de cas, le premier motif correspondrait si la valeur était Nothing, et le second correspondait si la valeur était construite avec Just. Si le second correspond, il associe également le nom val au paramètre qui a été transmis au constructeur Just lors de la construction de la valeur que vous comparez.

Que signifie peut-être

Peut-être connaissez-vous déjà comment cela fonctionne? il n'y a pas vraiment de magie dans les valeurs Maybe, il s'agit simplement d'un type de données algébriques (ADT) Haskell normal Mais il est assez utilisé car il "soulève" ou étend un type, tel que Integer à partir de votre exemple, dans un nouveau contexte dans lequel il possède une valeur supplémentaire (Nothing) qui représente un manque de valeur! Le système de typographie exige ensuite que vous vérifiiez cette valeur supplémentaire avant de vous permettre d’obtenir au Integer que pourrait être présent. Cela évite un nombre remarquable de bugs.

Aujourd'hui, de nombreuses langues traitent 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 que son "erreur de 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é un moyen efficace de le faire.

Peut-être en tant que foncteur

L'idée de transformer un type en un autre de telle sorte que les opérations sur l'ancien type puissent également être transformées pour fonctionner sur le nouveau type est le concept à la base de Haskell. classe de type appelée Functor, dont Maybe a a une instance utile de.

Functor fournit une méthode appelée fmap, qui mappe les fonctions qui vont des valeurs du type de base (telles que Integer) aux fonctions qui vont des valeurs du type levé (comme comme Maybe Integer). Une fonction transformée avec fmap pour travailler sur une valeur Maybe 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 fonction Maybe Integerm_x Et une fonction Int -> Intf, vous pouvez utiliser fmap f m_x Pour appliquer la fonction f directement au Maybe Integer sans se soucier de savoir si elle a réellement une valeur ou non. En fait, vous pouvez appliquer toute la chaîne de fonctions levées Integer -> Integer Aux valeurs Maybe Integer Et ne plus avoir à vous soucier de la vérification explicite de Nothing une fois que vous avez terminé.

Peut-être en tant que monade

Je ne sais pas encore si vous connaissez bien le concept de Monad, mais vous avez au moins déjà utilisé IO a Auparavant, et la signature de type IO a Est remarquablement similaire à Maybe a. Bien que IO ait ceci de particulier qu'il ne vous expose pas ses constructeurs et qu'il ne peut donc être "exécuté" que par le système d'exécution Haskell, il s'agit toujours d'un Functor en plus d'être un Monad. En fait, il y a un sens important dans lequel un Monad est juste un type spécial de Functor avec quelques fonctionnalités supplémentaires, mais ce n'est pas l'endroit pour entrer dans cela.

Quoi qu'il en soit, les monades aiment IO mapper les types sur de nouveaux types qui représentent des "calculs qui donnent des valeurs" et vous pouvez élever des fonctions dans les types Monad via une fonction très fmap appelée liftM qui transforme une fonction régulière en "calcul qui donne la valeur obtenue en évaluant la fonction".

Vous avez probablement deviné (si vous avez lu jusqu'à présent) que Maybe est aussi un Monad. Il représente "des calculs qui pourraient ne pas renvoyer de valeur". Comme avec l'exemple fmap, cela vous permet de faire un tas de calculs sans avoir à vérifier explicitement les erreurs après chaque étape. Et en fait, de la manière dont l’instance Monad est construite, un calcul sur Maybe valeurs s’arrête dès qu’un Nothing est rencontré, donc c'est un peu comme un avortement immédiat ou un retour sans valeur au milieu d'un calcul.

Vous auriez pu écrire peut-être

Comme je l'ai déjà dit, il n'y a rien d'inhérent au type Maybe qui soit intégré à la syntaxe du langage ou au système d'exécution. Si Haskell ne le fournissait pas par défaut, vous pourriez fournir toutes ses fonctionnalités vous-même! En fait, vous pourriez quand même le réécrire vous-même, sous des noms différents, et bénéficier des mêmes fonctionnalités.

J'espère que vous comprenez maintenant le type Maybe et ses constructeurs, mais s'il y a encore quelque chose d'incertain, faites le moi savoir!

191
Levi Pearson

La plupart des réponses actuelles sont des explications très techniques du fonctionnement de Just et de ses amis; J'ai pensé que je pourrais essayer d'expliquer à quoi ça sert.

Beaucoup de langages ont une valeur comme null qui peut être utilisée à la place d'une valeur réelle, du moins pour certains types. Cela a rendu beaucoup de gens très en colère et a été largement considéré comme un mauvais geste. Pourtant, il est parfois utile d'avoir une valeur telle que null pour indiquer l'absence de quelque chose.

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

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

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

36
Brent Royal-Gordon

Étant donné un type t, une valeur de Just t est une valeur existante de type t, où Nothing représente un échec pour atteindre une valeur ou un cas dans lequel une valeur n'aurait aucun sens.

Dans votre exemple, avoir un solde négatif n'a pas de sens, et si cela se produisait, il serait remplacé par Nothing.

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

13
qaphla

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

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

Autres conceptions traitant de fonctions non totales:

  • lancer des exceptions lors de la vérification de la valeur d'entrée ne correspond pas à la plage
  • renvoyer 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)
  • renvoyer une valeur spéciale (pointeur): NULL ou une telle
  • rentrer silencieusement sans rien faire: par exemple, lend pourrait être écrit pour renvoyer l'ancien solde, si la condition de prêt n'est pas remplie
  • renvoyer une valeur spéciale: Nothing (ou à gauche enveloppant un objet de description d'erreur)

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

Le problème avec le renvoi d'une valeur spéciale ou le lancement d'exceptions est qu'il est facile pour l'appelant d'omettre le traitement d'une telle possibilité par erreur.

Le problème de l'élimination silencieuse d'une défaillance est également évident: vous limitez ce que l'appelant peut faire avec la fonction. Par exemple, si lend a renvoyé l'ancien solde, l'appelant n'a aucun moyen de savoir si le solde a changé. Cela peut être ou ne pas être un problème, selon le but recherché.

La solution de Haskell oblige l'appelant d'une fonction partielle à traiter un type comme Maybe a, ou Either error a en raison du type de retour de la fonction.

De cette façon, lend tel qu'il est défini, est une fonction qui ne calcule pas toujours le nouvel équilibre - dans certaines circonstances, un nouvel équilibre n'est pas défini. Nous signalons cette circonstance à l'appelant en renvoyant la valeur spéciale Nothing ou en encapsulant le nouveau solde dans Just. L’appelant a maintenant la liberté de choisir: soit gérer l’échec de la procédure de prêt, soit ignorer et utiliser l’ancien solde - par exemple, maybe oldBalance id $ lend amount oldBalance.

2
Sassa NF