coldwa.st
Tous les guidesProgrammationWebDonnéesOutilsBases de donnéesHaskellConceptsCabal & buildsChaîne d’outilsCompilateurPerformanceÉditeur & HLS

Haskell · concepts · évaluation

L’évaluation paresseuse en Haskell, expliquée

Par ColdwastMis à jour le 14 juin 20268 min de lecture#haskell#laziness#concepts
Du code source à l’écran
L’évaluation paresseuse : Haskell calcule une valeur seulement au moment où quelque chose en a réellement besoin.

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

Lignes de code sur un écran sombre
Lignes de code à l’écran — la paresse vous laisse définir des structures infinies et n’évaluer jamais que la partie que vous consommez.
  • 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 filter et map sur 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 && undefined renvoie False sans jamais toucher undefined, 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 — force a en forme normale de tête faible avant de renvoyer b.
  • ($!) — application stricte : f $! x force x avant d’appliquer f.
  • BangPatterns — l’extension {-# LANGUAGE BangPatterns #-} vous laisse écrire let !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.

Guide indépendant, maintenu par la communauté. coldwa.st est un site de ressources de programmation ; cet article est un écrit explicatif neuf et original sur l’évaluation paresseuse en Haskell, sans affiliation avec aucun projet ni rédigé par lui. Le code reflète le comportement standard de Haskell/GHC — vérifiez auprès de la documentation GHC actuelle.