Vol. 8 · № 03 · Un atelier RubyA Ruby workshop

lazy & les Enumerator, ou le calcul tiré par la demande lazy & Enumerators, or computation pulled by demand

Une chaîne stricte matérialise un Array à chaque maillon ; lazy diffère et fait descendre les éléments un à un, ne produisant que ce qu'on demande — d'où l'infini calculable. Sous lazy, l'Enumerator (next, peek, Enumerator.new). Le piège : lazy sur dix éléments, où l'overhead dépasse le gain. A strict chain materializes an Array at every link; lazy defers and sends elements down one by one, producing only what's asked — hence computable infinity. Beneath lazy, the Enumerator (next, peek, Enumerator.new). The trap: lazy on ten elements, where overhead beats the gain.

AudienceAudience
Dev qui chaîne des transformations et veut différer à bon escient Dev chaining transformations who wants to defer wisely
Format
Self-paced
ChapitresChapters
5
Date
Juil 2026 Jul 2026
≈ 18 min ●●●○ lazyEnumeratorSéquences infinies
SommaireContents ·
№ 01CadrageFraming3 min

Une chaîne stricte alloue à chaque maillon — lazy diffère tout.A strict chain allocates at every link — lazy defers it all.

Le raffinement, dernier étage du volume. Une chaîne map.select.first est lisible, mais chaque maillon construit un tableau intermédiaire complet avant de passer le relais : map alloue tout, puis select alloue tout, et first n'en retient que cinq. Sur un million d'éléments, ce sont deux tableaux intermédiaires — un million de cases, puis encore plus de trois cent mille — pour cinq résultats ; sur une séquence infinie, c'est impossible — la première étape ne finit jamais. lazy renverse l'ordre : rien n'est calculé tant qu'on ne demande pas, et la donnée traverse alors la chaîne un élément à la fois, jusqu'à ce que first(5) dise stop. Le calcul suit la demande, pas la source.The refinement, the volume's top floor. A map.select.first chain is readable, but each link builds a full intermediate array before handing over: map allocates everything, then select allocates everything, and first keeps only five. On a million elements, that's two intermediate arrays — a million slots, then over three hundred thousand more — for five results; on an infinite sequence, it's impossible — the first step never finishes. lazy reverses the order: nothing is computed until you ask, and the data then crosses the chain one element at a time, until first(5) says stop. The computation follows the demand, not the source.

Strict alloue tout ; lazy ne calcule que le demandéStrict allocates all; lazy computes only what's asked
# strict : chaque maillon construit un Array COMPLET, puis le suivant
(1..1_000_000)
  .map    { |x| x * 2 }        # alloue un Array d'un million…
  .select { |x| x % 3 == 0 }   # … puis un autre Array…
  .first(5)                    # … pour n'en garder que CINQ

# lazy : rien ne se calcule avant first(5), élément par élément
(1..Float::INFINITY).lazy      # une séquence INFINIE, sans broncher
  .map    { |x| x * 2 }
  .select { |x| x % 3 == 0 }
  .first(5)                    # => [6, 12, 18, 24, 30]  5 éléments calculés
Par étage, ou par élémentBy stage, or by element
strictstrictmap TOUT → select TOUT → first 5map ALL → select ALL → first 52 gros tableaux intermédiaires2 big intermediate arrays
lazylazy1 élément → map → select → … ×5, puis stop1 element → map → select → … ×5, then stop0 tableau intermédiaire0 intermediate array

Strict avance par étage : il finit chaque méthode sur toute la collection avant la suivante. Lazy avance par élément : un seul descend toute la chaîne, puis le prochain — jusqu'à ce qu'on en ait assez.Strict advances by stage: it finishes each method over the whole collection before the next. Lazy advances by element: one goes down the whole chain, then the next — until you've had enough.

Le réflexe du numéroThe issue's reflex

« Cette chaîne crée-t-elle des tableaux intermédiaires que lazy rendrait inutiles ? » Si la source est énorme ou infinie, et qu'on n'en consomme qu'une partie (un first, un find, un take), lazy évite des allocations massives. Sinon — c'est le piège — il coûte plus qu'il ne rapporte."Does this chain create intermediate arrays that lazy would make needless?" If the source is huge or infinite, and you consume only part of it (a first, a find, a take), lazy avoids massive allocations. Otherwise — that's the trap — it costs more than it saves.

Le sommet du volume.The volume's summit.

On a fondé (un each débloque tout), composé (chaîner les transformations) ; voici le raffinement. lazy ne change pas ce que la chaîne calcule — seulement quand et combien. C'est la première fois du volume qu'on optimise, et fidèle à l'esprit de la série, on le fait pour une raison nommable : éviter des intermédiaires qui comptent, ou rendre l'infini calculable. Jamais par réflexe.We founded (one each unlocks all), composed (chaining transformations); here's the refinement. lazy doesn't change what the chain computes — only when and how much. It's the volume's first optimization, and true to the series' spirit, we do it for a nameable reason: avoiding intermediates that matter, or making the infinite computable. Never by reflex.

№ 02La surpriseThe surprise4 min

L'infini devient calculable — quand la demande tire le calcul.The infinite becomes computable — when demand pulls the computation.

La surprise du numéro : lazy ne fait pas que gagner des allocations, il rend possible ce qui ne l'était pas. Une chaîne stricte sur une séquence infinie ne rend jamais la main — map veut d'abord mapper tout l'infini. En lazy, la logique s'inverse : c'est le consommateur final (first, take, find) qui réclame des éléments, et la chaîne n'en produit que juste assez pour le satisfaire. Le calcul est tiré par la demande, pas poussé par la source. Du coup, (1..∞).lazy compose comme un tableau ordinaire, et first(3) arrête la production dès le troisième résultat trouvé.The issue's surprise: lazy doesn't only save allocations, it makes possible what wasn't. A strict chain over an infinite sequence never returns — map wants to map all of infinity first. In lazy, the logic flips: it's the final consumer (first, take, find) that requests elements, and the chain produces only just enough to satisfy it. The computation is pulled by demand, not pushed by the source. So (1..∞).lazy composes like an ordinary array, and first(3) stops production at the third result found.

map veut tout l'infini avant de rendre la mainmap wants all of infinity before returning
# strict sur l'infini : la première étape ne finit JAMAIS
(1..Float::INFINITY)
  .map { |x| x * x }       # tente de mapper… l'infini.
  .first(3)
# le programme part en boucle, ne rend rien, mange la mémoire :
# map veut son Array complet AVANT de passer la main à first

En strict, chaque méthode matérialise son résultat complet avant la suivante. Sur (1..Float::INFINITY), map n'a pas de « résultat complet » : il boucle sans fin, la mémoire enfle, first(3) n'est jamais atteint. La composition fonctionnelle, si élégante au № 2, se heurte ici au mur de l'évaluation stricte.In strict mode, each method materializes its full result before the next. On (1..Float::INFINITY), map has no "full result": it loops forever, memory swells, first(3) is never reached. The functional composition, so elegant in № 2, hits the wall of strict evaluation here.

Strict pousse la source vers le résultat ; lazy tire le résultat depuis la source — et n'en prend que ce qu'il faut.Strict pushes the source toward the result; lazy pulls the result from the source — taking only what's needed.
№ 03La mécaniqueThe mechanics5 min

L'Enumerator : l'itération devenue objet — qu'on tire ou qu'on confie.The Enumerator: iteration become an object — pulled or handed over.

Sous lazy se cache l'Enumerator : un objet qui réifie le parcours lui-même. Appelle each sans bloc et tu en reçois un — un parcours mis en pause, qu'on fait avancer à la demande avec next, ou qu'on inspecte avec peek sans bouger. Enumerator.new te laisse fabriquer tes propres séquences en yieldant des valeurs (y << n), y compris infinies via une boucle. Et tout Enumerator devient paresseux avec .lazy, donc composable sur l'infini comme au chapitre précédent. C'est la même itération vue de deux côtés : externe quand on tire les éléments un à un (next), interne quand on confie un bloc (each, map). L'Enumerator est l'objet qui rend les deux possibles.Beneath lazy sits the Enumerator: an object that reifies traversal itself. Call each with no block and you get one — a paused traversal you advance on demand with next, or inspect with peek without moving. Enumerator.new lets you build your own sequences by yielding values (y << n), infinite ones included via a loop. And any Enumerator becomes lazy with .lazy, hence composable over infinity as in the previous chapter. It's the same iteration seen from two sides: external when you pull elements one by one (next), internal when you hand over a block (each, map). The Enumerator is the object making both possible.

next/peek, et Enumerator.new pour ses séquencesnext/peek, and Enumerator.new for your sequences
# un each SANS bloc rend un Enumerator : un objet-itération réifié
e = [10, 20, 30].each      # => #<Enumerator>
e.next                     # => 10   on tire un élément…
e.peek                     # => 20   on regarde le suivant SANS avancer
e.next                     # => 20   … puis on avance

# Enumerator.new : fabriquer sa propre séquence, même infinie
naturals = Enumerator.new do |y|
  n = 1
  loop { y << n; n += 1 }   # y << valeur : « produit cet élément »
end
naturals.next              # => 1
naturals.next              # => 2
lazy sur un Enumerator maisonlazy on a custom Enumerator
# un Enumerator est paresseux-able : on compose sur l'infini
naturals.lazy
  .select { |n| n % 7 == 0 }
  .map    { |n| n * n }
  .first(3)                # => [49, 196, 441]

# next/peek = itération EXTERNE (on tire) ; each/map = interne (on confie)
# les deux faces du même Enumerator — selon qui mène la boucle
Itération interne vs externe.Internal vs external iteration.

Depuis le Vol 2, on confie un bloc et la méthode mène la boucle — l'itération interne (each, map). L'Enumerator ajoute l'autre face : externe, où toi tires les éléments quand tu veux (next, peek) — pour entrelacer deux séquences, faire une pause, regarder devant. Enumerator.new + .lazy est le combo qui produit des flux infinis composables : un générateur, en somme. Mais garde la juste mesure — c'est puissant, rarement nécessaire au quotidien.Since Vol 2, you hand over a block and the method drives the loop — internal iteration (each, map). The Enumerator adds the other face: external, where you pull elements when you want (next, peek) — to interleave two sequences, pause, look ahead. Enumerator.new + .lazy is the combo producing composable infinite streams: a generator, in effect. But keep proportion — it's powerful, rarely needed day to day.

Boucle avec le Vol 1 : un Enumerator est un objet comme un autre, à qui l'on envoie next, peek, map, lazy. L'itération n'est pas une construction du langage — c'est un objet qu'on manipule, qu'on stocke, qu'on passe en argument. Même la boucle, en Ruby, est un message.A loop back to Vol 1: an Enumerator is an object like any other, to which you send next, peek, map, lazy. Iteration isn't a language construct — it's an object you manipulate, store, pass as an argument. Even the loop, in Ruby, is a message.

№ 04Le piègeThe trap4 min

lazy sur dix éléments : l'overhead coûte plus que le gain.lazy on ten elements: the overhead costs more than the gain.

Le contre-exemple obligatoire, et l'erreur du dev fraîchement converti à lazy : le dégainer partout. Sur une petite collection qu'on consomme entièrement, lazy est plus lent que la chaîne stricte — et pour rien. Chaque élément doit traverser l'enrobage Enumerator::Lazy, fait d'objets et d'appels indirects, là où map et select strictes tournent dans des boucles C optimisées. Le seul bénéfice de lazy — éviter les tableaux intermédiaires — ne vaut que si ces tableaux sont gros, ou si on s'arrête tôt. Deux Array de dix cases ne coûtent rien ; les éviter au prix de l'overhead Enumerator est une perte sèche. lazy n'est pas « la version rapide » : c'est l'outil de l'énorme, de l'infini, et de l'arrêt anticipé.The mandatory counter-example, and the freshly-converted dev's mistake: reaching for lazy everywhere. On a small collection consumed fully, lazy is slower than the strict chain — and for nothing. Each element must cross the Enumerator::Lazy wrapper, made of objects and indirect calls, where strict map and select run in optimized C loops. lazy's only benefit — avoiding intermediate arrays — is worth it only if those arrays are big, or if you stop early. Two ten-slot arrays cost nothing; avoiding them at the price of Enumerator overhead is a pure loss. lazy isn't 'the fast version': it's the tool of the huge, the infinite, and early termination.

lazy là où il n'y a rien à différerlazy where there's nothing to defer
# Le piège : lazy sur une petite collection consommée en entier
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  .lazy
  .map { |x| x * 2 }
  .select { |x| x % 3 == 0 }
  .to_a                    # => [6, 12, 18]  on matérialise TOUT de toute façon

# coût caché : chaque élément traverse l'enrobage Enumerator::Lazy —
# objets et appels indirects — pour économiser deux Array de 10 cases
# parfaitement négligeables. Plus lent, pour aucun gain réel.
Strict gagne sur le petit consommé en entierStrict wins on the small, fully consumed
# Strict : deux petits intermédiaires, mais des boucles C optimisées
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  .map { |x| x * 2 }
  .select { |x| x % 3 == 0 }   # => [6, 12, 18]
                               # plus rapide ET plus simple à cette taille

# la règle : lazy paie quand…
#   · la source est énorme OU infinie
#   · ET on n'en consomme qu'une PARTIE (first, take, find, lazy.first)
# sinon, sur du petit consommé en entier, le strict gagne toujours.
Le contre-exemple obligatoire.The mandatory counter-example.

Deux conditions, toutes deux requises, pour que lazy paie : la source est énorme ou infinie, et tu n'en consommes qu'une partie (un first, un find, un take). L'une sans l'autre, et le strict gagne : sur du petit, l'overhead de l'Enumerator écrase le gain ; sur du gros consommé en entier, lazy diffère un travail qu'on fera de toute façon, en plus lent. Fidèle au fil de la série : on n'optimise qu'avec une raison — ici, des intermédiaires qui pèsent vraiment, ou un infini à dompter. Sinon, la chaîne stricte du № 2 reste le bon défaut.Two conditions, both required, for lazy to pay off: the source is huge or infinite, and you consume only part of it (a first, a find, a take). One without the other, and strict wins: on the small, the Enumerator overhead crushes the gain; on the big consumed fully, lazy defers work you'll do anyway, slower. True to the series' thread: optimize only with a reason — here, intermediates that really weigh, or an infinity to tame. Otherwise, № 2's strict chain stays the right default.

lazy n'est pas « la version rapide » des chaînes — c'est l'outil de l'énorme, l'infini, et l'arrêt anticipé. Ailleurs, il ralentit.lazy isn't "the fast version" of chains — it's the tool of the huge, the infinite, and early stops. Elsewhere, it slows you down.
№ 05Bilan & éditoWrap-up & editorial2 min

Diffère quand l'énorme ou l'infini le réclame — pas avant.Defer when the huge or the infinite calls for it — not before.

Une chaîne stricte matérialise un tableau à chaque maillon ; lazy diffère, et fait descendre les éléments un à un, ne produisant que ce que le consommateur réclame. Cela rend l'infini calculable et évite les intermédiaires massifs. Sous lazy, l'Enumerator : l'itération réifiée en objet, qu'on tire (next, peek) ou qu'on confie (each), et qu'on fabrique soi-même (Enumerator.new). Mais le gain a un seuil : sur du petit consommé en entier, l'overhead dépasse l'économie. lazy paie si la source est énorme ou infinie ET qu'on s'arrête tôt — sinon le strict gagne.A strict chain materializes an array at each link; lazy defers, sending elements down one at a time, producing only what the consumer requests. This makes infinity computable and avoids massive intermediates. Beneath lazy, the Enumerator: iteration reified as an object, pulled (next, peek) or handed over (each), and built yourself (Enumerator.new). But the gain has a threshold: on the small consumed fully, overhead exceeds savings. lazy pays if the source is huge or infinite AND you stop early — otherwise strict wins.

La situationThe situationL'outilThe toolLe réflexeThe reflex
Petite collection, consommée en entierSmall collection, fully consumedChaîne stricte : map / select / reduce.Strict chain: map / select / reduce.Le défaut. lazy ne ferait que ralentir.The default. lazy would only slow it.
Source énorme + arrêt anticipéHuge source + early stop.lazy … .first / .find / .take..lazy … .first / .find / .take.On évite des allocations qui comptent vraiment.You avoid allocations that truly matter.
Séquence infinieInfinite sequence(1..Float::INFINITY).lazy, Enumerator.new.(1..Float::INFINITY).lazy, Enumerator.new.Le seul moyen : strict boucle sans fin.The only way: strict loops forever.
Tirer les éléments un à unPull elements one by oneL'Enumerator : next, peek.The Enumerator: next, peek.Itération externe : rare, mais là quand il faut.External iteration: rare, but there when needed.
lazy ne calcule que ce qu'on lui demande. Le tout est de n'en demander ni trop tôt, ni quand il n'y a rien à différer.lazy computes only what you ask. The art is asking neither too early, nor when there's nothing to defer.
Mot de l'éditeurFrom the editor

Fin du Vol 8 : un each, un style, et un raffinement mesuré.End of Vol 8: one each, a style, and a measured refinement.

Le volume a monté trois marches. La fondation : écris un each, hérite cinquante méthodes — Enumerable n'a besoin que d'un parcours. Le style : pense en transformations chaînées, qui nomment l'intention que la boucle cachait. Le raffinement : lazy diffère le calcul, dompte l'infini, évite les intermédiaires — mais seulement quand ils pèsent. Le fil n'est pas « le fonctionnel, c'est mieux » ; c'est une montée disciplinée où chaque cran a sa raison, et où le dernier — l'optimisation — ne se prend qu'avec un motif nommable. C'est exactement la transition vers le Vol 9 : on a effleuré, avec lazy, l'idée qu'une liberté a un coût mesurable. Le dernier volume en fait sa seule affaire.The volume climbed three steps. The foundation: write an each, inherit fifty methods — Enumerable needs only a traversal. The style: think in chained transformations, naming the intent the loop hid. The refinement: lazy defers the work, tames infinity, avoids intermediates — but only when they weigh. The thread isn't 'functional is better'; it's a disciplined ascent where each rung has its reason, and the last — optimization — is taken only with a nameable motive. That's exactly the transition into Vol 9: with lazy, we brushed the idea that a freedom has a measurable cost. The final volume makes it its sole business.

Prochain volume : le coût du dynamisme. On a passé huit volumes à goûter la liberté de Ruby — tout est message, tout est objet, tout se manipule. Le Vol 9 en nomme le prix : la résolution de méthode et son cache, les allocations cachées et le GC, le GVL et la concurrence. Règle de la série : on n'y entre qu'avec un profil — benchmark, memory_profiler, GC.stat — jamais avec une intuition. La flexibilité n'est pas gratuite ; le dernier volume apprend à la mesurer avant de la payer.Next volume: the cost of dynamism. We spent eight volumes savoring Ruby's freedom — everything is a message, everything an object, everything manipulable. Vol 9 names its price: method resolution and its cache, hidden allocations and the GC, the GVL and concurrency. The series' rule: enter only with a profile — benchmark, memory_profiler, GC.stat — never with an intuition. Flexibility isn't free; the final volume learns to measure it before paying.