Haskell : un excellent langage procédural
(entropicthoughts.com)Traiter les effets de bord comme des valeurs de première classe
- En Haskell, les effets de bord (par ex. la génération de nombres aléatoires, l’affichage, etc.) sont traités comme des « valeurs de première classe » (
first class value) - Autrement dit, un appel de fonction qui produit un effet de bord, comme
randomRIO(1, 6), ne renvoie pas directement un résultat, mais un « objet qui décrit une action à exécuter un jour » - Cet objet produira effectivement une valeur aléatoire lorsqu’il sera exécuté, mais avant cela il ne contient qu’un plan d’exécution
- Un type comme
IO Intreprésente une « action qui, lorsqu’elle est réellement exécutée, produira unInt» ; elle ne s’exécute pas immédiatement au moment de l’appel, mais plus tard, au moment opportun - Grâce à cette propriété, contrairement aux langages procéduraux traditionnels où « appel de fonction = exécution immédiate », Haskell permet de composer des effets de bord puis de les exécuter plus tard
Démystifier les blocs do
- Un bloc
don’est pas une syntaxe magique : il est en réalité composé de deux opérateurs qui permettent d’enchaîner (bind) les effets de bord et de les exécuter dans l’ordre (then)
then
- L’opérateur
*>exécute l’effet de bord de gauche, ignore sa valeur de résultat, puis exécute l’effet de bord de droite - Par exemple,
putStr "hello" *> putStrLn "world"crée une unique actionIO ()qui combine les deux affichages dans l’ordre - Quand on écrit plusieurs lignes dans un bloc
do, cette exécution séquentielle repose en interne sur ce type d’opérateur
bind
- L’opérateur
>>=exécute l’effet de bord de gauche puis transmet la valeur obtenue à la fonction de droite - Exemple :
randomRIO(1, 6) >>= print_sidecrée un effet de bord qui transmet le résultat du dé àprint_sidepour l’afficher - Dans un bloc
do, la syntaxe<-est une manière plus pratique d’exprimer cette opération
Deux opérateurs suffisent pour les blocs do
- En fin de compte, un bloc
doest construit à partir de ces deux opérateurs :*>et>>= - La syntaxe
doest très utilisée pour sa lisibilité et sa simplicité, mais pour mieux tirer parti de Haskell, il faut aussi exploiter des fonctions de composition d’effets de bord plus riches
Fonctions qui opèrent sur les effets de bord
- La bibliothèque standard fournit plusieurs fonctions permettant de manipuler les effets de bord de façon plus variée
pure
pure xcrée une « action qui produit la valeurxsans aucun effet de bord supplémentaire »- Exemple :
loaded_die = pure 4crée unIO Intqui renvoie toujours 4
fmap
- Avec une forme
fmap :: (a -> b) -> IO a -> IO b, elle crée une action qui applique une fonction pure à la valeur résultant d’un effet de bord afin de produire une nouvelle valeur - Exemple : avec
length <$> getEnv "HOME", on peut créer une action qui récupère une variable d’environnement puis appliquelengthpour en calculer la longueur
liftA2, liftA3, …
- Des fonctions comme
liftA2etliftA3combinent les résultats de plusieurs effets de bord avec une fonction pure pour produire un nouvel effet de bord - Exemple :
liftA2 (+) (randomRIO(1,6)) (randomRIO(1,6))crée un effet de bord qui additionne les valeurs de deux dés - On peut aussi faire la même chose avec une combinaison de
<$>et<*>
Intermède : à quoi bon ?
- Cela peut sembler n’être qu’une fonctionnalité simple, possible aussi dans d’autres langages, mais en Haskell on peut à tout moment extraire une action à effet de bord dans une variable ou la recomposer sans que son moment d’exécution ni son résultat ne changent
- En traitant les effets de bord de manière indépendante, on réduit la confusion lors du refactoring et on permet une réutilisation sûre fondée sur le raisonnement équationnel (
equational reasoning)
sequenceA
sequenceA [IO a] -> IO [a]transforme une « liste d’actions avec effets de bord » en une « unique action avec effet de bord qui produit une liste de résultats »- Par exemple, on peut rassembler plusieurs actions
logdans une liste, puis les exécuter d’un coup plus tard avecsequenceA - Même des effets de bord répétés à l’infini (par ex.
repeat (randomRIO(1,6))) peuvent être stockés dans une liste, puis exécutés avecsequenceAaprès avoir pris seulement lesnpremiers viatake n
Interlude : fonctions utilitaires
void,sequenceA_,replicateM,replicateM_, etc. sont pratiques lorsqu’on n’utilise pas la valeur de retour ou lorsqu’on veut répéter une exécution- Exemple : avec
replicateM_ 500 (putStrLn "I will not cheat again."), on peut exécuter plusieurs fois un effet de bord sans compter soi-même les itérations
traverse
traverse :: (a -> IO b) -> [a] -> IO [b]crée une action qui applique une fonction à effet de bord à chaque élément d’une liste, puis rassemble les résultats dans une listesequenceAest en fait équivalent àtraverse id, ettraverse_en est la version qui ignore les résultats
for
-
fora la même fonction quetraverse, mais prend ses arguments dans l’ordre inverse -
Exemple : avec une forme comme
for numbers $ \n -> ..., on peut exprimer naturellement quelque chose qui ressemble à une bouclefor -
Grâce à ce type de composition, des opérations comme les répétitions, les parcours ou les transformations de structures de données — qui nécessitent dans d’autres langages une syntaxe dédiée — peuvent en Haskell être implémentées par composition de fonctions de bibliothèque
Exploiter pleinement le caractère de première classe des effets
- En Haskell, exploiter activement les effets de bord comme des valeurs de première classe permet de réduire la duplication de code et d’améliorer la structure
- Par exemple, dans une logique de factorisation de grands nombres avec cache, on peut utiliser
Stateà la place deIOpour construire une structure où « des effets existent, mais n’ont pas d’impact sur l’extérieur » - De cette manière, les effets de bord structurés ne sont appliqués qu’aux parties nécessaires, tandis que le reste du code peut rester constitué de fonctions pures, ce qui assure à la fois sûreté et flexibilité
- Au final,
evalStateet consorts permettent d’exécuter les effets et d’obtenir un résultat sous forme de valeur pure
Ce dont vous n’avez jamais besoin de vous soucier
- Plusieurs noms hérités des débuts de Haskell (
>>,return,mapM, etc.) peuvent aujourd’hui être remplacés par des fonctions modernes (*>,pure,traverse, etc.) - Ils viennent d’anciens noms ou d’une conception centrée sur les monades ; aujourd’hui, on recommande plutôt une approche basée sur
Applicativeou, plus généralement, surFunctor
Annexe A : éviter le succès et l’inutilité
- La formule « Haskell avoids success » signifie que le langage ne sacrifie pas ses valeurs fondamentales au profit de la popularité ou de la commodité
- « Haskell is useless » renvoie au fait qu’au départ, en n’autorisant que des fonctions purement pures, le langage donnait l’impression de ne rien permettre de faire ; il a ensuite gagné en praticité grâce à l’introduction d’une manière de traiter les effets de bord comme des éléments « de première classe »
Annexe B : pourquoi fmap s’applique à la fois aux effets de bord et aux listes
fmapa une forme très générale (Functor f => (a -> b) -> f a -> f b) et s’applique de manière commune à différents conteneurs ou types d’effets comme les listes,MaybeouIO- Appliqué à une liste,
fmapapplique la fonction à tous les éléments ; appliqué àIO, il applique la fonction à la valeur de résultat - Plus largement, toute « structure à laquelle on peut appliquer une fonction » est appelée un
Functor
Annexe C : Foldable et Traversable
Foldabledésigne une structure dont on peut parcourir et traiter les élémentsTraversabledésigne une structure que l’on peut non seulement parcourir, mais aussi reconstruire avec de nouveaux éléments tout en conservant la même forme- Pour que
sequenceAoutraversepuissent collecter des valeurs en conservant la structure d’origine, cette structure doit êtreTraversable - Des structures comme les arbres ou les
Setpeuvent voir leur forme dépendre des valeurs ; on distingue donc les cas où seul le parcours est possible (Foldable) et ceux où l’on peut réellement reconstruire la structure (Traversable) - Selon les besoins, on peut aussi convertir en liste puis utiliser
traverse, ce qui permet de gérer les effets de bord de façon flexible
2 commentaires
Sur Reddit, on voit beaucoup de pubs... Mais rien que le nom crée une sorte de barrière psychologique.
On a l’impression que c’est un langage très difficile et très puissant...
Avis Hacker News
Le système de types de Haskell est complexe par rapport à d’autres langages populaires. En particulier, des opérateurs comme
*>,<*>et<*augmentent la courbe d’apprentissage dans l’ensemble de la base de code>>=et>>pour rester productifHaskell aide à améliorer la programmation impérative
La version généralisée de
traverse/mapMfonctionne non seulement pour les listes, mais pour tous les typesTraversable, et elle est très utiletraverse :: Applicative f => (a -> f b) -> t a -> f (t b)Haskell dispose de monades puissantes, ce qui le rend plus procédural
doParmi les logiciels écrits en Haskell, on trouve ImplicitCAD
Le code Haskell se lit comme celui d’un langage procédural, tout en offrant les avantages du travail avec des fonctions à effets de bord
>>est l’ancien nom de<i>>, et les deux opérateurs sont associatifs à gauche>>est défini commeinfixl 1et<i>>commeinfixl 4, donc<i>>se lie plus fortement que>>IO aetaen Haskell peuvent donner une impression similaire à l’asynchrone et au synchroneDans d’autres langages, on peut faire des IO simples avec une fonction comme
console.log("abc")Les personnes qui n’ont jamais essayé Haskell peuvent trouver le Haskell réel, avec les extensions GHC, trop complexe