3 points par GN⁺ 2025-01-20 | 2 commentaires | Partager sur WhatsApp

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 Int représente une « action qui, lorsqu’elle est réellement exécutée, produira un Int » ; 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 do n’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 action IO () 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_side crée un effet de bord qui transmet le résultat du dé à print_side pour 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 do est construit à partir de ces deux opérateurs : *> et >>=
  • La syntaxe do est 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 x crée une « action qui produit la valeur x sans aucun effet de bord supplémentaire »
  • Exemple : loaded_die = pure 4 crée un IO Int qui 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 applique length pour en calculer la longueur

liftA2, liftA3, …

  • Des fonctions comme liftA2 et liftA3 combinent 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 log dans une liste, puis les exécuter d’un coup plus tard avec sequenceA
  • 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 avec sequenceA après avoir pris seulement les n premiers via take 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 liste
  • sequenceA est en fait équivalent à traverse id, et traverse_ en est la version qui ignore les résultats

for

  • for a la même fonction que traverse, 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 boucle for

  • 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 de IO pour 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, evalState et 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 Applicative ou, plus généralement, sur Functor

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

  • fmap a 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, Maybe ou IO
  • Appliqué à une liste, fmap applique 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

  • Foldable désigne une structure dont on peut parcourir et traiter les éléments
  • Traversable dé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 sequenceA ou traverse puissent collecter des valeurs en conservant la structure d’origine, cette structure doit être Traversable
  • Des structures comme les arbres ou les Set peuvent 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

 
bbulbum 2025-01-21

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...

 
GN⁺ 2025-01-20
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

    • Si on n’utilise pas Haskell pendant un mois, il faut réétudier des opérateurs comme >>= et >> pour rester productif
    • Il est difficile d’apprendre les concepts de Haskell seul, sans en discuter avec d’autres personnes
  • Haskell aide à améliorer la programmation impérative

    • Les effets de première classe et les patterns permettent d’éliminer le code boilerplate
    • Grâce à la sûreté du typage, on peut écrire rapidement du code relativement exempt de bugs
  • La version généralisée de traverse/mapM fonctionne non seulement pour les listes, mais pour tous les types Traversable, et elle est très utile

    • Elle peut être utilisée sous la forme traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
    • Dans d’autres langages, il fallait écrire manuellement beaucoup de code pour obtenir un effet similaire
  • Haskell dispose de monades puissantes, ce qui le rend plus procédural

    • On peut utiliser des variables intermédiaires dans les blocs do
  • Parmi 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

    • Travailler avec la monade IO est complexe, et cela devient encore plus compliqué lorsqu’on veut utiliser d’autres types de monades
  • >> est l’ancien nom de <i>>, et les deux opérateurs sont associatifs à gauche

    • >> est défini comme infixl 1 et <i>> comme infixl 4, donc <i>> se lie plus fortement que >>
  • IO a et a en Haskell peuvent donner une impression similaire à l’asynchrone et au synchrone

    • Le premier renvoie une promesse/future qu’il faut attendre
  • Dans d’autres langages, on peut faire des IO simples avec une fonction comme console.log("abc")

    • Il y a des interrogations sur ce qui différencie cela des IO en Haskell
  • Les personnes qui n’ont jamais essayé Haskell peuvent trouver le Haskell réel, avec les extensions GHC, trop complexe

    • Cela peut réduire l’intérêt pour Haskell