En tant que pratiquant, pourquoi devrais-je me soucier d'Haskell? Qu'est-ce qu'une monade et pourquoi en ai-je besoin? [fermé]


9

Je ne comprends tout simplement pas quel problème ils résolvent.



2
Je pense que ce montage est un peu extrême. Je pense que votre question était essentiellement bonne. C'est juste que certaines parties étaient un peu ... argumentatives. Ce qui n'est probablement que le résultat de la frustration d'essayer d'apprendre quelque chose dont vous ne voyiez pas l'intérêt.
Jason Baker

@SnOrfus, c'est moi qui ai bâtardé la question. J'étais trop paresseux pour le modifier correctement.
Job

Réponses:


34

Les monades ne sont ni bonnes ni mauvaises. Ils le sont. Ce sont des outils qui sont utilisés pour résoudre des problèmes comme de nombreuses autres constructions de langages de programmation. Une application très importante d'entre eux est de faciliter la vie des programmeurs travaillant dans un langage purement fonctionnel. Mais ils sont utiles dans les langages non fonctionnels; c'est juste que les gens réalisent rarement qu'ils utilisent une Monade.

Qu'est-ce qu'une monade? La meilleure façon de penser à une monade est un modèle de conception. Dans le cas des E / S, vous pourriez probablement le considérer comme étant un peu plus qu'un pipeline glorifié où l'état global est ce qui se passe entre les étapes.

Par exemple, prenons le code que vous écrivez:

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

Il se passe beaucoup plus de choses ici qu'il n'y paraît. Par exemple, vous remarquerez que putStrLna la signature suivante: putStrLn :: String -> IO (). Pourquoi est-ce?

Pensez-y de cette façon: supposons (par souci de simplicité) que stdout et stdin sont les seuls fichiers que nous pouvons lire et écrire. Dans un langage impératif, ce n'est pas un problème. Mais dans un langage fonctionnel, vous ne pouvez pas muter l'état global. Une fonction est simplement quelque chose qui prend une valeur (ou des valeurs) et renvoie une valeur (ou des valeurs). Une façon de contourner cela consiste à utiliser l'état global comme valeur qui est transmise à chaque fonction. Vous pouvez donc traduire la première ligne de code en quelque chose comme ceci:

global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state

... et le compilateur saurait imprimer tout ce qui est ajouté au deuxième élément de global_state. Maintenant, je ne sais pas pour vous, mais je détesterais programmer comme ça. La façon dont cela a été rendu plus facile était d'utiliser les Monades. Dans une Monade, vous passez une valeur qui représente une sorte d'état d'une action à l'autre. C'est pourquoi putStrLna un type de retour de IO (): il renvoie le nouvel état global.

Alors, pourquoi vous en souciez-vous? Eh bien, les avantages de la programmation fonctionnelle par rapport au programme impératif ont été débattus à plusieurs reprises, donc je ne vais pas répondre à cette question en général (mais consultez cet article si vous voulez entendre le cas de la programmation fonctionnelle). Pour ce cas spécifique, il pourrait être utile de comprendre ce que Haskell essaie d'accomplir.

Beaucoup de programmeurs pensent que Haskell essaie de les empêcher d'écrire du code impératif ou d'utiliser des effets secondaires. Ce n'est pas tout à fait vrai. Pensez-y de cette façon: un langage impératif est celui qui autorise les effets secondaires par défaut, mais vous permet d'écrire du code fonctionnel si vous le souhaitez vraiment (et êtes prêt à faire face à certaines des contorsions qui nécessiteraient). Haskell est purement fonctionnel par défaut, mais vous permet d'écrire du code impératif si vous le souhaitez vraiment (ce que vous faites si votre programme est utile). Le but n'est pas de rendre difficile l'écriture de code qui a des effets secondaires. C'est pour vous assurer que vous êtes explicite sur les effets secondaires (avec le système de type qui applique cela).


6
Ce dernier paragraphe est de l'or. Pour l'extraire et le paraphraser un peu: "Un langage impératif est un langage qui autorise les effets secondaires par défaut, mais vous permet d'écrire du code fonctionnel si vous le voulez vraiment. Un langage fonctionnel est purement fonctionnel par défaut, mais vous permet d'écrire du code impératif si tu veux vraiment. "
Frank Shearar

Il convient de noter que l'article que vous avez lié rejette spécifiquement l'idée de «l'immuabilité en tant que vertu de la programmation fonctionnelle» dès le début.
Mason Wheeler

@MasonWheeler: J'ai lu ces paragraphes, non pas comme rejetant l'importance de l'immuabilité, mais comme un argument convaincant pour démontrer la supériorité de la programmation fonctionnelle. En fait, il dit la même chose au sujet de l'élimination de goto(comme argument pour une programmation structurée) un peu plus loin dans le document, qualifiant ces arguments de "infructueux". Et pourtant, aucun de nous ne souhaite secrètement gotole retour. C'est simplement que vous ne pouvez pas dire que ce goton'est pas nécessaire pour les personnes qui l'utilisent intensivement.
Robert Harvey

7

Je vais mordre !!! Les monades en elles-mêmes ne sont pas vraiment une raison d'être pour Haskell (les premières versions de Haskell n'en avaient même pas).

Votre question est un peu comme dire "C ++ quand je regarde la syntaxe, je m'ennuie tellement. Mais les modèles sont une fonctionnalité très annoncée de C ++ donc j'ai regardé une implémentation dans un autre langage".

L'évolution d'un programmeur Haskell est une blague, elle n'est pas destinée à être prise au sérieux.

Une Monade aux fins d'un programme dans Haskell est une instance de la classe de type Monade, c'est-à-dire qu'il s'agit d'un type qui prend en charge un certain petit ensemble d'opérations. Haskell a un support spécial pour les types qui implémentent la classe de type Monad, en particulier le support syntaxique. En pratique, il en résulte ce que l'on a appelé un «point-virgule programmable». Lorsque vous combinez cette fonctionnalité avec certaines des autres fonctionnalités de Haskell (fonctions de première classe, paresse par défaut), vous obtenez la possibilité d'implémenter certaines choses en tant que bibliothèques qui étaient traditionnellement considérées comme des fonctionnalités de langage. Vous pouvez par exemple implémenter un mécanisme d'exception. Vous pouvez implémenter la prise en charge des continuations et des coroutines en tant que bibliothèque. Haskell, le langage ne prend pas en charge les variables mutables:

Vous demandez "Peut-être / Identité / Monades de division sûre ???". La monade Maybe est un exemple de la façon dont vous pouvez implémenter (très simple, une seule exception) la gestion des exceptions en tant que bibliothèque.

Vous avez raison, écrire des messages et lire les entrées des utilisateurs n'est pas très unique. IO est un exemple moche de «monades en tant que fonctionnalité».

Mais pour itérer, une "fonctionnalité" en elle-même (par exemple Monades) isolée du reste du langage ne semble pas nécessairement immédiatement utile (une grande nouvelle fonctionnalité de C ++ 0x est les références rvalue, ne signifie pas que vous pouvez prendre les sortir du contexte C ++ car sa syntaxe vous ennuie et voit forcément l'utilitaire). Un langage de programmation n'est pas quelque chose que vous obtenez en lançant un tas de fonctionnalités dans un compartiment.


En fait, haskell prend en charge les variables mutables via la monade ST (l'une des rares parties magiques impures étranges du langage qui joue selon ses propres règles).
sara

4

Les programmeurs écrivent tous des programmes, mais les similitudes s'arrêtent là. Je pense que les programmeurs diffèrent beaucoup plus que la plupart des programmeurs ne peuvent l'imaginer. Prenez n'importe quelle «bataille» de longue date, comme le typage de variables statiques vs les types d'exécution uniquement, les scripts vs compilés, le style C vs orienté objet. Il vous sera impossible d'argumenter rationnellement qu'un camp est inférieur, car certains d'entre eux génèrent un excellent code dans un système de programmation qui me semble inutile ou même carrément inutilisable.

Je pense que différentes personnes pensent simplement différemment, et si vous n'êtes pas tenté par le sucre syntaxique ou en particulier les abstractions qui n'existent que pour des raisons de commodité et qui ont en fait un coût d'exécution important, alors restez loin de ces langues.

Je recommanderais cependant que vous essayiez au moins de vous familiariser avec les concepts que vous abandonnez. Je n'ai rien contre quelqu'un qui est résolument pro-pur-C, tant qu'il comprend réellement le gros problème des expressions lambda. Je soupçonne que la plupart ne deviendront pas immédiatement un fan, mais au moins ce sera là à l'arrière de leur esprit quand ils trouveront le problème parfait, ce qui aurait été tellement plus facile à résoudre avec les lambdas.

Et, surtout, essayez d'éviter de vous ennuyer en parlant de fanboy, surtout par des gens qui ne savent pas de quoi ils parlent.


4

Haskell applique la transparence référentielle : étant donné les mêmes paramètres, chaque fonction renvoie toujours le même résultat, quel que soit le nombre de fois que vous appelez cette fonction.

Cela signifie, par exemple, que sur Haskell (et sans Monades), vous ne pouvez pas implémenter un générateur de nombres aléatoires. En C ++ ou Java, vous pouvez le faire en utilisant des variables globales, en stockant la valeur "seed" intermédiaire du générateur aléatoire.

Sur Haskell, l'équivalent des variables globales sont les Monades.


Alors ... et si vous vouliez un générateur de nombres aléatoires? N'est-ce pas aussi une fonction? Même si ce n'est pas le cas, comment puis-je me procurer un générateur de nombres aléatoires?
Job

@Job Vous pouvez créer un générateur de nombres aléatoires à l'intérieur d'une monade (c'est essentiellement un tracker d'état), ou vous pouvez utiliser unsafePerformIO, le diable de Haskell qui ne devrait jamais être utilisé (et en fait, il cassera probablement votre programme si vous utilisez l'aléatoire à l'intérieur!)
alternative

À Haskell, vous passez soit un «RandGen» qui est essentiellement l'état actuel du RNG. Ainsi, la fonction qui génère un nouveau nombre aléatoire prend un RandGen et retourne un tuple avec le nouveau RandGen et le nombre produit. L'alternative est de spécifier quelque part que vous voulez une liste de nombres aléatoires entre une valeur min et une valeur max. Cela renverra un flux infini de nombres évalués paresseusement, nous pouvons donc simplement parcourir cette liste chaque fois que nous avons besoin d'un nouveau nombre aléatoire.
Qqwy

De la même manière que vous les obtenez dans n'importe quelle autre langue! vous vous emparez d'un algorithme générateur de nombres pseudo-aléatoires, puis vous l'amorcez avec une certaine valeur et récoltez les nombres "aléatoires" qui sortent! La seule différence est que des langages comme C # et Java amorcent automatiquement le PRNG pour vous en utilisant l'horloge système ou des trucs comme ça. Cela et le fait que dans haskell vous obtenez également un nouveau PRNG que vous pouvez utiliser pour obtenir le numéro "suivant", alors qu'en C # / Java, tout cela se fait en interne en utilisant des variables mutables dans l' Randomobjet.
sara

4

C'est une vieille question, mais elle est vraiment bonne, alors je vais répondre.

Vous pouvez considérer les monades comme des blocs de code pour lesquels vous avez un contrôle total sur la façon dont ils sont exécutés: ce que chaque ligne de code doit retourner, si l'exécution doit s'arrêter à tout moment, si un autre traitement doit avoir lieu entre chaque ligne.

Je vais donner quelques exemples de choses que les monades permettent et qui seraient difficiles autrement. Aucun de ces exemples n'est en Haskell, juste parce que mes connaissances en Haskell sont un peu chancelantes, mais ce sont tous des exemples de la façon dont Haskell a inspiré l'utilisation des monades.

Analyseurs

Normalement, si vous vouliez écrire un analyseur quelconque, par exemple pour implémenter un langage de programmation, vous devriez soit lire la spécification BNF et écrire tout un tas de code en boucle pour l'analyser, soit utiliser un compilateur compilateur comme Flex, Bison, yacc etc .. Mais avec les monades, vous pouvez faire une sorte de "compilateur analyseur" directement dans Haskell.

Les analyseurs ne peuvent pas vraiment se faire sans monades ni langages spéciaux comme le yacc, le bison, etc.

Par exemple, j'ai pris la spécification du langage BNF pour le protocole IRC :

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                ; any octet except NUL, CR, LF, " " and ":"
middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

SPACE      =  %x20        ; space character
crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

Et je l'ai réduit à environ 40 lignes de code en F # (qui est un autre langage qui prend en charge les monades):

type UserIdentifier = { Name : string; User: string; Host: string }

type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }

let space = character (char 0x20)

let parameters =
    let middle = parser {
        let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
        let! cs = many <| sat ((<>)(char 0x20))
        return (c::cs)
    }
    let trailing = many item
    let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
    many parameter

let command = atLeastOne letter +++ (count 3 digit)

let prefix = parser {
    let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20))   //this is more lenient than RFC2812 2.3.1
    let! uh = parser {
        let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
        let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
        return (user, host)
    }
    let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
    return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}

let message = parser {
    let! p = maybe (parser {
        let! _ = character ':'
        let! p = prefix
        let! _ = space
        return p
    })
    let! c = command
    let! ps = parameters
    return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}

La syntaxe monade de F # est assez moche par rapport à celle de Haskell, et j'aurais probablement pu améliorer cela un peu - mais le point à retenir est que structurellement, le code de l'analyseur est identique au BNF. Non seulement cela aurait pris beaucoup plus de travail sans monades (ou générateur de parseurs), mais cela n'aurait eu presque aucune ressemblance avec les spécifications, et aurait donc été terrible à lire et à maintenir.

Multitâche personnalisé

Normalement, le multitâche est considéré comme une fonctionnalité du système d'exploitation - mais avec les monades, vous pouvez écrire votre propre planificateur de telle sorte qu'après chaque monade d'instructions, le programme passe le contrôle au planificateur, qui choisit ensuite une autre monade à exécuter.

Un gars a créé une monade "tâche" pour contrôler les boucles de jeu (encore une fois en F #), de sorte qu'au lieu d'avoir à tout écrire comme une machine d'état qui agit à chaque Update()appel, il pourrait simplement écrire toutes les instructions comme s'il s'agissait d'une seule fonction .

En d'autres termes, au lieu d'avoir à faire quelque chose comme:

class Robot
{
   enum State { Walking, Shooting, Stopped }

   State state = State.Stopped;

   public void Update()
   {
      switch(state)
      {
         case State.Stopped:
            Walk();
            state = State.Walking;
            break;
         case State.Walking:
            if (enemyInSight)
            {
               Shoot();
               state = State.Shooting;
            }
            break;
      }
   }
}

Vous pourriez faire quelque chose comme:

let robotActions = task {
   while (not enemyInSight) do
      Walk()
   while (enemyInSight) do
      Shoot()
}

LINQ to SQL

LINQ to SQL est en fait un exemple de monade, et des fonctionnalités similaires pourraient facilement être implémentées dans Haskell.

Je n'entrerai pas dans les détails car je ne me souviens pas de tout cela avec précision, mais Erik Meijer l'explique assez bien .


1

Si vous connaissez les modèles GoF, les monades sont comme le modèle Decorator et le modèle Builder réunis, sur des stéroïdes, mordus par un blaireau radioactif.

Il y a de meilleures réponses ci-dessus, mais certains des avantages spécifiques que je vois sont:

  • les monades décorent un type de noyau avec des propriétés supplémentaires sans changer le type de noyau. Par exemple, une monade peut "soulever" une chaîne et ajouter des valeurs comme "isWellFormed", "isProfanity" ou "isPalindrome" etc.

  • de même, les monades permettent de conglomérer un type simple en un type de collection

  • les monades permettent la liaison tardive des fonctions dans cet espace d'ordre supérieur

  • les monades permettent de mélanger des fonctions et des arguments arbitraires avec un type de données arbitraire, dans l'espace d'ordre supérieur

  • les monades permettent de mélanger des fonctions pures et sans état avec une base impure et avec état, de sorte que vous pouvez garder une trace de l'endroit où le problème est

Un exemple familier d'une monade en Java est List. Il prend une classe de base, comme String, et la "lève" dans l'espace monade de List, ajoutant des informations sur la liste. Ensuite, il lie de nouvelles fonctions dans cet espace comme get (), getFirst (), add (), empty (), etc.

À grande échelle, imaginez qu'au lieu d'écrire un programme, vous venez d'écrire un gros Builder (comme le modèle GoF), et la méthode build () à la fin crache la réponse que le programme était censé produire. Et que vous pourriez ajouter de nouvelles méthodes à votre ProgramBuilder sans recompiler le code d'origine. C'est pourquoi les monades sont un modèle de conception puissant.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.