47 votes

Devriez-vous éviter la notation do en Haskell?

La plupart des tutoriels Haskell enseignent l'utilisation de la notation do pour IO.

J'ai aussi commencé avec la notation do, mais cela fait que mon code ressemble plus à un langage impératif qu'à un langage FP.

Cette semaine, j'ai vu un tutoriel utiliser IO avec <$>

stringAnalyzer <$> readFile "testfile.txt"

au lieu d'utiliser do

main = do
    strFile <- readFile "testfile.txt"
    let analysisResult = stringAnalyzer strFile
    return analysisResult

Et l'outil d'analyse de logs est terminé sans le do.

Alors ma question est "Devrions-nous éviter la notation do dans tous les cas?".

Je sais que peut-être do rendra le code meilleur dans certains cas.

Aussi, pourquoi la plupart des tutoriels enseignent-ils IO avec do?

À mon avis, <$> et <*> rendent le code plus FP que IO.

0 votes

Q2 : Les tutoriels doivent-ils enseigner l'IO avec do parce que (a) l'applicative est plus récente et moins connue, (b) la notation do est plus proche du code impératif, que beaucoup d'étudiants apprennent en premier, et (c) il y a certaines choses qu'on peut faire avec un Monad qu'on ne peut pas faire avec une applicative, principalement changer quel code est appelé en fonction de la valeur précédemment sortie.

0 votes

Q1: Applicative est agréable, propre et très fonctionnel dans le style. Maîtrisez-le et soyez à l'affût du plus grand nombre d'opportunités possibles pour nettoyer votre code avec un peu de *>, mais vous devrez parfois utiliser toute la puissance des monades et lorsque vous le ferez, la notation do est généralement la plus claire.

0 votes

Utilisez-le seulement lorsque cela facilite les choses. Par exemple, n'utilisez jamais la notation "do" pour une seule ligne. (Comme, ne faites jamais main = do print "Hello World". Utilisez plutôt main = print "Hello World".) Assurez-vous de bien comprendre les monades en termes de >>= et return. Lorsque vous avez bien compris cela, vous pouvez utiliser do.

48voto

jozefg Points 29997

do notation in Haskell se désugère d'une manière assez simple.

do
  x <- foo
  e1 
  e2
  ...

se transforme en

 foo >>= \x ->
 do
   e1
   e2

et

do
  x
  e1
  e2
  ...

en

x >>
do 
  e1
  e2
  ....

Cela signifie que vous pouvez vraiment écrire n'importe quelle computation monadique avec >>= et return. La seule raison pour laquelle nous ne le faisons pas est que c'est juste une syntaxe plus douloureuse. Les monades sont utiles pour imiter du code impératif, la notation do lui donne cet aspect.

La syntaxe de type C le rend beaucoup plus facile à comprendre pour les débutants. Vous avez raison, cela ne semble pas aussi fonctionnel, mais exiger de quelqu'un qu'il comprenne correctement les monades avant de pouvoir utiliser IO est un gros obstacle.

La raison pour laquelle nous utilisons >>= et return, d'un autre côté, c'est parce que c'est beaucoup plus compact pour les lignes de code de 1 à 2. Cependant, cela devient un peu plus difficile à lire pour des choses trop grandes. Donc, pour répondre directement à votre question, non ne évitez pas la notation do quand c'est approprié.

Enfin, les deux opérateurs que vous avez vus, <$> et <*>, sont en fait fmap et applicatifs respectivement, pas monadiques. Ils ne peuvent pas être utilisés pour représenter beaucoup de ce que fait la notation do. Ils sont plus compacts, c'est certain, mais ils ne vous permettent pas de nommer facilement des valeurs intermédiaires. Personnellement, je les utilise environ 80% du temps, surtout parce que j'ai tendance à écrire des fonctions très petites et composable de toute façon, ce pour quoi les applicatifs sont excellents.

3 votes

Le désucréage que vous décrivez est en effet simple, mais naïf. La notation do implique également la gestion des erreurs liée à la fonction fail de Monad, mais vous n'en parlez pas du tout. Néanmoins, cette réponse a été très utile et je l'ai référencée plusieurs fois. +1 de ma part.

0 votes

Cela m'a été utile. Cependant, quel est le cas de base ?

47voto

leftaroundabout Points 23679

À mon avis, <$> et <*> rendent le code plus FP que IO.

Haskell n'est pas un langage purement fonctionnel parce que cela "a meilleure allure". Parfois oui, souvent non. La raison de rester fonctionnel n'est pas sa syntaxe mais sa sémantique. Cela nous dote de transparence référentielle, ce qui rend beaucoup plus facile de prouver des invariants, permet des optimisations très haut niveau, rend facile d'écrire du code polyvalent, etc..

Rien de tout cela n'a beaucoup à voir avec la syntaxe. Les calculs monadiques restent toujours purement fonctionnels - peu importe que vous les écriviez avec la notation do ou avec <$>, <*> et >>=, donc nous obtenons les avantages de Haskell de toute façon.

Cependant, malgré les avantages FP mentionnés ci-dessus, il est souvent plus intuitif de penser aux algorithmes d'un point de vue impératif - même si vous êtes habitué à comment cela est implémenté à travers les monades. Dans ces cas, la notation do vous donne cette compréhension rapide de "l'ordre de calcul", "l'origine des données", "le point de modification", mais il est trivial de désugar manuellement dans votre tête vers la version >>=, pour comprendre ce qui se passe de manière fonctionnelle.

Le style applicatif est certainement excellent à bien des égards, cependant il est intrinsèquement point-free. C'est souvent une bonne chose, mais surtout dans des problèmes plus complexes, il peut être très utile de donner des noms à des variables "temporaires". Lorsque vous utilisez uniquement la syntaxe "FP" Haskell, cela nécessite soit des lambdas, soit des fonctions nommées explicitement. Les deux ont de bons cas d'utilisation, mais le premier introduit assez de bruit en plein milieu de votre code et le second perturbe plutôt le "flux" car il nécessite un where ou un let placé ailleurs que là où vous l'utilisez. do, en revanche, vous permet d'introduire une variable nommée exactement là où vous en avez besoin, sans introduire de bruit du tout.

18 votes

J'endosse cette réponse. Éviter la notation do juste parce qu'elle "semble impérative" est contre-productif (cela rend souvent le code beaucoup moins lisible). Utilisez la bonne outil syntaxe pour le travail, etc.

42voto

Benjamin Hodgson Points 3510

Je me retrouve souvent à écrire d'abord une action monadique en notation do, puis à la refactoriser en une simple expression monadique (ou functorielle). Cela se produit surtout quand le bloc do s'avère plus court que prévu. Parfois, je refactorise dans l'autre sens; cela dépend du code en question.

Ma règle générale est la suivante : si le bloc do ne comporte que quelques lignes, il est généralement plus propre sous forme d'expression courte. Un long bloc do est probablement plus lisible tel quel, à moins que vous ne trouviez un moyen de le diviser en fonctions plus petites et plus composables.


À titre d'exemple concret, voici comment nous pourrions transformer votre extrait de code verbeux en une version plus simple.

main = do
    strFile <- readFile "testfile.txt"
    let analysisResult = stringAnalyzer strFile
    return analysisResult

Tout d'abord, remarquez que les deux dernières lignes ont la forme let x = y in return x. Cela peut bien sûr être transformé en simplement return y.

main = do
    strFile <- readFile "testfile.txt`
    return (stringAnalyzer strFile)

Il s'agit d'un bloc do très court : nous associons readFile "testfile.txt" à un nom, puis faisons quelque chose avec ce nom à la ligne suivante. Essayons de le 'désucrer' comme le ferait le compilateur :

main = readFile "testFile.txt" >>= \strFile -> return (stringAnalyser strFile)

Regardez la forme lambda du côté droit de >>=. Il réclame d'être réécrit en style point-free : \x -> f $ g x devient \x -> (f . g) x qui devient f . g.

main = readFile "testFile.txt" >>= (return . stringAnalyser)

C'est déjà beaucoup plus net que le bloc do original, mais nous pouvons aller plus loin.

Voici la seule étape qui demande un peu de réflexion (mais une fois familiarisé avec les monades et les foncteurs, cela devrait être évident). La fonction ci-dessus est évocatrice de l'une des lois des monades : (m >>= return) == m. La seule différence est que la fonction du côté droit de >>= n'est pas seulement return - nous faisons quelque chose à l'objet à l'intérieur de la monade avant de le renvoyer dans un return. Mais le motif de 'faire quelque chose à une valeur enveloppée sans affecter son enveloppe' correspond exactement à ce que Functor est censé faire. Toutes les monades sont des foncteurs, nous pouvons donc refactoriser cela pour ne même pas avoir besoin de l'instance Monad :

main = fmap stringAnalyser (readFile "testFile.txt")

Enfin, notez que <$> est juste une autre façon d'écrire fmap.

main = stringAnalyser <$> readFile "testFile.txt"

Je pense que cette version est beaucoup plus claire que le code original. Il peut être lu comme une phrase : "main est stringAnalyser appliqué au résultat de la lecture de "testFile.txt"". La version originale vous embourbe dans les détails procéduraux de son fonctionnement.


Addendum : mon commentaire selon lequel 'toutes les monades sont des foncteurs' peut en fait être justifié par l'observation que m >>= (return . f) (alias liftM de la bibliothèque standard) est équivalent à fmap f m. Si vous avez une instance de Monad, vous obtenez gratuitement une instance de Functor - il vous suffit de définir fmap = liftM ! Si quelqu'un a défini une instance de Monad pour son type mais n'a pas défini d'instances pour Functor et Applicative, je considérerais cela comme un bogue. Les clients s'attendent à pouvoir utiliser les méthodes de Functor sur les instances de Monad sans trop de tracas.

5 votes

Heureusement, la base 4.8 rend Applicative (et donc Functor) une superclasse de Monad, le grand changement révolutionnaire dont personne ne se plaint jamais. Donc bientôt, il ne sera même plus nécessaire de penser à vérifier si votre Monad est un Functor !

10voto

cheecheeo Points 359

Le style applicatif devrait être encouragé car il compose (et c'est plus joli). Le style monadique est nécessaire dans certains cas. Voir https://stackoverflow.com/a/7042674/1019205 pour une explication approfondie.

6voto

Sergey Bolgov Points 556

do notation est juste un sucre syntaxique. Il peut être évité dans tous les cas. Cependant, dans certains cas, remplacer do par >>= et return rend le code moins lisible.

Donc, pour vos questions :

"Devrions-nous éviter l'instruction Do dans tous les cas ?".

Concentrez-vous sur la clarification et l'amélioration de la lisibilité de votre code. Utilisez do quand cela aide, évitez-le sinon.

Et j'avais une autre question, pourquoi la plupart des tutoriels enseignent-ils l'entrée-sortie avec do ?

Parce que do rend le code d'entrée-sortie plus lisible dans de nombreux cas.

De plus, la majorité des personnes qui commencent à apprendre Haskell ont de l'expérience en programmation impérative. Les tutoriels sont destinés aux débutants. Ils devraient utiliser un style facile à comprendre pour les nouveaux venus.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X