Haskell · concepts · évaluation
L’évaluation paresseuse en Haskell, expliquée
La plupart des langages sont stricts : une expression est évaluée dès qu’elle est liée à un nom. Haskell est différent — il est paresseux (plus précisément non strict) par défaut : une expression n’est pas évaluée tant que son résultat n’est pas véritablement nécessaire. Ce seul choix de conception est derrière certains des pouvoirs les plus distinctifs de Haskell et son piège le plus notoire. Ce guide explique ce qu’est l’évaluation paresseuse, comment fonctionnent les thunks, ce qu’elle vous apporte, et comment la désactiver quand il le faut.
L’idée en une phrase
Sous l’évaluation paresseuse, lier une valeur ne la calcule pas — Haskell stocke un calcul différé (un thunk) et ne le force que lorsqu’un autre calcul exige le résultat. Si le résultat n’est jamais exigé, le travail n’est jamais fait.
Thunks : le calcul différé
Quand vous écrivez let x = expensive 42, Haskell n’exécute pas expensive. Il crée un thunk — une promesse non évaluée de expensive 42. Le thunk n’est forcé que lorsque quelque chose a besoin de la valeur réelle de x (par exemple l’afficher ou faire du pattern matching dessus). Forcez-le une fois, et le résultat est mis en cache pour ne pas être recalculé.
let x = expensive 42 -- nothing runs yet; x is a thunk
let y = x + 1 -- still nothing; y is a thunk too
print y -- NOW x and y are forced and evaluated Ce que la paresse vous apporte
- Structures de données infinies.
[1..]est la liste infinie des entiers.take 5 [1..]renvoie[1,2,3,4,5]parce que seuls les cinq premiers sont jamais exigés. - Composition sans gaspillage. Vous pouvez enchaîner
filteretmapsur une liste énorme et Haskell fusionne le travail, ne produisant que ce que le consommateur tire — pas de listes intermédiaires complètes si elles ne sont jamais entièrement exigées. - Court-circuit gratuit.
False && undefinedrenvoieFalsesans jamais toucherundefined, parce que le second argument n’est jamais forcé. - Auto-référence. Des one-liners classiques comme
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)définissent le flux infini de Fibonacci — possible uniquement parce que la structure est construite paresseusement.
Le piège : les fuites de mémoire
La paresse a un coût. Les thunks non évalués s’accumulent en mémoire, et une longue chaîne de calculs différés peut faire silencieusement gonfler l’usage du tas — une fuite de mémoire (space leak). Le cas d’école est un fold à gauche paresseux qui construit une tour géante de thunks (((0 + 1) + 2) + 3) ... au lieu d’additionner au fur et à mesure :
foldl (+) 0 [1..1000000] -- builds a million thunks, may blow the stack
foldl' (+) 0 [1..1000000] -- strict fold: adds as it goes, constant space La solution est de forcer l’évaluation là où ça compte. Recourez au fold à gauche strict foldl' (de Data.List) pour les accumulations.
Forcer la rigueur : seq, $! et BangPatterns
Haskell vous donne des outils précis pour exiger l’évaluation :
seq a b— forceaen forme normale de tête faible avant de renvoyerb.($!)— application stricte :f $! xforcexavant d’appliquerf.BangPatterns— l’extension{-# LANGUAGE BangPatterns #-}vous laisse écrirelet !x = ...pour forcer une liaison.
Une subtilité à connaître : forcer atteint typiquement la forme normale de tête faible (le constructeur le plus externe), pas l’évaluation complète. Pour forcer en profondeur, utilisez force / deepseq du paquet deepseq.
FAQ
Haskell est-il paresseux ou non strict ? Techniquement non strict (une garantie sémantique) ; GHC l’implémente via l’évaluation paresseuse avec thunks et partage. En pratique on dit « paresseux ».
Pourquoi mon programme Haskell utilise-t-il autant de mémoire ? Souvent une fuite de mémoire due à des thunks accumulés — couramment un foldl paresseux. Passez à foldl' ou ajoutez de la rigueur avec seq/BangPatterns.
Qu’est-ce qu’un thunk ? Un calcul non évalué et différé stocké à la place d’une valeur, forcé seulement quand la valeur est exigée, puis mis en cache.
La paresse rend-elle Haskell lent ? Pas intrinsèquement — elle peut éviter du travail inutile et permettre la fusion. Mais une paresse négligente cause des fuites de mémoire ; savoir quand forcer fait partie de l’écriture d’un bon Haskell.
La paresse est la même mécanique qui fait que les autres abstractions de Haskell se composent proprement — voyez comment fonctionne le séquencement dans notre guide sur les monades en Haskell, et mettez en place la chaîne d’outils pour essayer ces exemples dans GHCi avec le guide du compilateur GHC.