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

Enumerable via each, ou le contrat minimal Enumerable via each, or the minimal contract

Implémente un seul each qui yielde tes éléments, include Enumerable, et tu hérites map, select, reduce, sort, group_by — une cinquantaine de méthodes, sur n'importe quelle structure. Le mixin appelle ton each, il n'a aucune autre source. Le piège : réécrire ces méthodes une par une. Implement a single each that yields your elements, include Enumerable, and you inherit map, select, reduce, sort, group_by — some fifty methods, on any structure. The mixin calls your each, it has no other source. The trap: rewriting these methods one by one.

AudienceAudience
Dev qui écrit une classe-collection et veut le vocabulaire fonctionnel Dev writing a collection class who wants the functional vocabulary
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●○○ EnumerableeachMixin
SommaireContents ·
№ 01CadrageFraming3 min

Écris un seul each — et hérite cinquante méthodes.Write a single each — and inherit fifty methods.

Le Vol 8 récolte le fruit du Vol 2. each n'est pas « la boucle de Ruby » : c'est une méthode à qui on confie un bloc, rappelé une fois par élément. Et c'est exactement le contrat que réclame le module Enumerable — un seul. include Enumerable dans ta classe, définis each en yieldant tes éléments, et tu reçois d'un coup map, select, reduce, sort, min_by, group_by, find, take_while… une cinquantaine de méthodes, toutes construites sur ton each. Tu n'écris pas une bibliothèque de collection : tu déclares comment parcourir, et Enumerable fait le reste.Vol 8 reaps Vol 2's fruit. each isn't 'Ruby's loop': it's a method you hand a block, called once per element. And that's exactly the contract the Enumerable module asks for — just one. include Enumerable in your class, define each by yielding your elements, and you get at once map, select, reduce, sort, min_by, group_by, find, take_while… some fifty methods, all built on your each. You don't write a collection library: you declare how to traverse, and Enumerable does the rest.

Un each, et tout Enumerable s'ouvreOne each, and all of Enumerable opens
class Playlist
  include Enumerable          # le mixin (Vol 5) — il s'appuie sur each
  def initialize(songs) = @songs = songs

  def each(&block)            # LE seul contrat à fournir : yield chaque élément
    @songs.each(&block)
  end
end

list = Playlist.new(songs)
list.map(&:title)            # gratuit
list.select(&:fav?)          # gratuit
list.min_by(&:duration)      # gratuit
list.group_by(&:genre)       # gratuit  — aucune n'a été écrite à la main
Le contrat minimalThe minimal contract
each
ce que TU écriswhat YOU write
→
include Enumerable
une ligneone line
→
≈ 50 méthodes
ce que tu REÇOISwhat you GET

Toutes les méthodes d'Enumerable n'ont besoin que d'une chose : pouvoir parcourir. Elles appellent ton each en interne. Fournis le parcours, hérite les transformations — c'est le meilleur rapport effort/pouvoir de tout Ruby.Every Enumerable method needs only one thing: to be able to traverse. They call your each internally. Provide the traversal, inherit the transformations — the best effort/power ratio in all of Ruby.

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

« Ma classe-collection a-t-elle un each idiomatique, ou je réinvente map à la main ? » Dès qu'un objet contient ou produit plusieurs éléments, donne-lui un each et include Enumerable — au lieu d'écrire, une par une, des méthodes que le mixin offrait déjà."Does my collection class have an idiomatic each, or am I reinventing map by hand?" The moment an object holds or produces several elements, give it an each and include Enumerable — instead of writing, one by one, methods the mixin already offered.

Le pont avec le Vol 2.The bridge to Vol 2.

Le Vol 2 a montré qu'each est une méthode qui rappelle un bloc. Le voici récompensé : ce même each, écrit une fois, est la clé de tout Enumerable. Et c'est le même mécanisme qu'au Vol 5 — include Enumerable insère le module dans ta chaîne d'ancêtres, ses méthodes deviennent les tiennes. Rien de neuf : juste deux briques des volumes précédents qui s'emboîtent.Vol 2 showed each is a method that calls a block back. Here it's rewarded: that same each, written once, is the key to all of Enumerable. And it's the same mechanism as Vol 5 — include Enumerable inserts the module into your ancestor chain, its methods become yours. Nothing new: just two bricks from earlier volumes clicking together.

№ 02La surpriseThe surprise4 min

Une méthode débloque cinquante — et sur n'importe quelle structure.One method unlocks fifty — and on any structure.

La surprise du numéro : Enumerable ne sait rien de tes données. Un tableau, un arbre, un flux réseau, une plage de dates — peu importe la structure interne. Tout ce qu'il demande, c'est un each qui yielde les éléments dans l'ordre que tu décides. Dès qu'il l'a, map, select, sort, include?, group_by fonctionnent sur ton objet comme sur un Array — alors que ton each fait un parcours d'arbre en profondeur. Tu n'as pas adapté tes données à une interface : tu as déclaré comment les parcourir, et tout le vocabulaire fonctionnel s'est greffé dessus.The issue's surprise: Enumerable knows nothing about your data. An array, a tree, a network stream, a date range — the internal structure doesn't matter. All it asks is an each that yields the elements in the order you decide. Once it has that, map, select, sort, include?, group_by work on your object just like on an Array — even though your each does a depth-first tree walk. You didn't adapt your data to an interface: you declared how to traverse it, and the whole functional vocabulary grafted onto that.

each existe, mais map n'est pas làeach exists, but map isn't there
class Tree
  def initialize(root) = @root = root
  def each(&block)             # un parcours en profondeur, par exemple
    walk(@root, &block)
  end
  private def walk(node, &block)
    return unless node
    yield node.value
    walk(node.left, &block)
    walk(node.right, &block)
  end
end

# sans include Enumerable : on n'a QUE each
Tree.new(root).map { |v| v * 2 }   # NoMethodError: map

L'arbre sait déjà se parcourir — each fait un joli DFS récursif. Mais sans le mixin, l'objet n'a que each : map lève NoMethodError. On a la moitié du contrat (le parcours), pas encore la récompense.The tree already knows how to traverse itself — each does a neat recursive DFS. But without the mixin, the object has only each: map raises NoMethodError. You have half the contract (the traversal), not yet the reward.

Enumerable ne connaît pas tes données — il connaît ton each. Déclare le parcours, hérite le vocabulaire.Enumerable doesn't know your data — it knows your each. Declare the traversal, inherit the vocabulary.
№ 03La mécaniqueThe mechanics5 min

Enumerable appelle ton each — et <=> débloque Comparable pareil.Enumerable calls your each — and <=> unlocks Comparable the same way.

La mécanique n'a rien de magique. Chaque méthode d'Enumerable est écrite en termes de each : map accumule yield(x), select garde ceux qui passent, reduce replie. Elles n'ont aucune autre source d'éléments que ton each — d'où la seule exigence : yielder un à un, dans l'ordre voulu. Le même motif se rejoue un cran plus bas avec Comparable : définis <=> sur ta classe d'élément, include Comparable, et tu obtiens <, >, ==, between?, clamp, plus le sort et le min d'Enumerable qui s'appuient dessus. Deux mixins canoniques, une même idée : un seul contrat débloque toute une famille.The mechanics are no magic. Every Enumerable method is written in terms of each: map accumulates yield(x), select keeps those that pass, reduce folds. They have no element source other than your each — hence the only requirement: yield one at a time, in the intended order. The same pattern plays one rung lower with Comparable: define <=> on your element class, include Comparable, and you get <, >, ==, between?, clamp, plus Enumerable's sort and min that rest on it. Two canonical mixins, one idea: a single contract unlocks a whole family.

map, en gros : il relaie ton each au blocmap, roughly: it relays your each to the block
# ce que fait, en gros, le map d'Enumerable — sur TON each
module Enumerable
  def map
    result = []
    each { |x| result << yield(x) }   # il appelle TON each, c'est tout
    result
  end
end

# d'où la règle : ton each doit yielder les éléments un à un, dans l'ordre.
# Enumerable ne fait que les relayer au bloc — il n'a aucune autre source.
Comparable : un <=> débloque l'ordreComparable: one <=> unlocks ordering
# même principe, un cran plus bas : <=> débloque Comparable sur l'ÉLÉMENT
class Version
  include Comparable                 # le frère d'Enumerable
  attr_reader :n
  def initialize(n) = @n = n
  def <=>(other) = n <=> other.n     # LE seul contrat de Comparable
  def inspect = n.to_s               # juste pour lire les sorties ci-dessous
end

v = [Version.new(3), Version.new(1), Version.new(2)]
v.sort                               # => [1, 2, 3]  sort utilise <=>
v.min                                # => 1
Version.new(1) < Version.new(2)      # => true  <, >, ==, between?, clamp : gratuits
Deux contrats, deux familles.Two contracts, two families.

Enumerable opère sur la collection (un each → 50 méthodes). Comparable opère sur l'élément (un <=> → tous les opérateurs d'ordre). Et ils se complètent : le sort d'Enumerable ordonne en demandant <=> à tes éléments — donc une collection d'objets Comparable se trie sans bloc. Le même principe du Vol 5 (un mixin canonique greffé par un contrat minimal), appliqué deux fois, à deux niveaux.Enumerable operates on the collection (one each → 50 methods). Comparable operates on the element (one <=> → all ordering operators). And they complete each other: Enumerable's sort orders by asking your elements for <=> — so a collection of Comparable objects sorts with no block. The same Vol 5 principle (a canonical mixin grafted by a minimal contract), applied twice, at two levels.

Un détail pratique : comme map, select & co retournent un Array (pas une instance de ta classe), tu repars d'un tableau ordinaire après transformation. Si tu veux qu'elles te rendent ton type, c'est le terrain de la composition du Vol 5 — mais pour 95 % des cas, hériter d'Enumerable et récupérer un Array est exactement ce qu'on veut.A practical detail: since map, select & co return an Array (not an instance of your class), you continue from a plain array after transforming. If you want them to hand back your type, that's Vol 5's composition ground — but for 95% of cases, inheriting Enumerable and getting an Array back is exactly what you want.

№ 04Le piègeThe trap4 min

Réécrire map, select, find à la main — une par une, à l'infini.Rewriting map, select, find by hand — one by one, forever.

Le contre-exemple obligatoire du numéro. On a une classe-collection, et on lui ajoute map. Puis on a besoin de select : on l'écrit aussi. Puis find, puis reduce, puis group_by… On réimplémente, une par une, des méthodes qu'un seul include Enumerable aurait toutes données — gratuites, déjà testées par l'équipe Ruby, cohérentes entre elles, et enrichies à chaque version du langage. Chaque méthode écrite à la main est du code à maintenir, à tester, et à garder synchrone avec ses cinquante cousines qu'on n'a pas écrites. Le réflexe : dès qu'on tape def map dans une classe qui sait se parcourir, c'est qu'on aurait dû taper include Enumerable.The issue's mandatory counter-example. You have a collection class, and you add map to it. Then you need select: you write that too. Then find, then reduce, then group_by… You reimplement, one by one, methods a single include Enumerable would have given you all — free, already tested by the Ruby team, consistent with each other, and enriched with every language version. Each hand-written method is code to maintain, to test, and to keep in sync with its fifty cousins you didn't write. The reflex: the moment you type def map in a class that knows how to traverse itself, you should have typed include Enumerable.

Une méthode écrite, quarante-neuf qui manquentOne method written, forty-nine still missing
# Le piège : réécrire à la main ce qu'Enumerable offrait
class Inventory
  def initialize(items) = @items = items

  def map
    out = []
    @items.each { |i| out << yield(i) }       # on réinvente map…
    out
  end
  def select
    out = []
    @items.each { |i| out << i if yield(i) }  # … et select…
    out
  end
  def find
    @items.each { |i| return i if yield(i) }  # … et find…
    nil
  end
  # et reduce ? group_by ? min_by ? sort_by ? partition ? on en réécrit 50 ?
end
Un each, et les cinquante arriventOne each, and the fifty arrive
# La même classe, le contrat minimal
class Inventory
  include Enumerable
  def initialize(items) = @items = items
  def each(&block) = @items.each(&block)      # tout tient ici
end

inv = Inventory.new(items)
inv.map { ... }      # ┐
inv.select { ... }   # │  les 50 méthodes, gratuites, testées par Ruby,
inv.find { ... }     # │  cohérentes entre elles, et à jour à chaque version
inv.group_by { ... } # ┘
Le contre-exemple obligatoire.The mandatory counter-example.

Réécrire map à la main n'est pas seulement plus long — c'est moins bon. Tu reproduis, avec tes bugs, ce que Ruby a déjà écrit, testé et optimisé ; tu n'auras jamais les quarante-neuf autres ; et tes versions divergeront du comportement attendu (gestion du bloc absent, valeurs de retour, Enumerator rendu sans bloc). include Enumerable + un each idiomatique te donne la famille entière, cohérente et maintenue ailleurs. La seule question à te poser : mon objet sait-il se parcourir ? Si oui, ne réimplémente rien.Rewriting map by hand isn't just longer — it's worse. You reproduce, with your bugs, what Ruby already wrote, tested and optimized; you'll never have the other forty-nine; and your versions will drift from expected behavior (no-block handling, return values, the Enumerator returned with no block). include Enumerable + an idiomatic each gives you the whole family, consistent and maintained elsewhere. The only question to ask: does my object know how to traverse itself? If yes, reimplement nothing.

Chaque def map écrit à la main est quarante-neuf méthodes qui manquent — et une de plus à déboguer.Every hand-written def map is forty-nine methods still missing — and one more to debug.
№ 05Bilan & éditoWrap-up & editorial2 min

Le parcours est à toi ; les transformations sont au mixin.The traversal is yours; the transformations are the mixin's.

Enumerable n'a besoin que d'un each qui yielde les éléments. Fournis-le, include Enumerable, et map, select, reduce, sort, group_by — une cinquantaine de méthodes — fonctionnent sur ta structure, fût-ce un arbre ou un flux. Le mixin appelle ton each, il n'a aucune autre source. Le même principe vaut pour Comparable : un <=> débloque l'ordre. Le piège, c'est de réécrire ces méthodes une à une au lieu de déclarer le seul contrat qui les donne toutes.Enumerable needs only an each that yields the elements. Provide it, include Enumerable, and map, select, reduce, sort, group_by — some fifty methods — work on your structure, be it a tree or a stream. The mixin calls your each, it has no other source. The same principle holds for Comparable: one <=> unlocks ordering. The trap is rewriting these methods one by one instead of declaring the single contract that gives them all.

La questionThe questionL'outilThe toolLe réflexeThe reflex
Mon objet contient des élémentsMy object holds elementsUn each + include Enumerable.An each + include Enumerable.Déclare le parcours, hérite 50 méthodes.Declare the traversal, inherit 50 methods.
Mes éléments ont un ordreMy elements have an orderUn <=> + include Comparable.A <=> + include Comparable.Un contrat, et <, >, sort, min suivent.One contract, and <, >, sort, min follow.
Je tape def map…I'm typing def map…Stop : Enumerable l'a déjà, testé et cohérent.Stop: Enumerable has it, tested and consistent.Réimplémenter le mixin est un anti-réflexe.Reimplementing the mixin is an anti-reflex.
map me rend un Arraymap hands me an ArrayNormal : les transformations sortent du type.Normal: transformations exit the type.Veux ton type ? composition (Vol 5).Want your type? composition (Vol 5).
Le meilleur rapport effort/pouvoir de Ruby : un each, cinquante méthodes. Écris le contrat, jamais la famille.Ruby's best effort/power ratio: one each, fifty methods. Write the contract, never the family.
Mot de l'éditeurFrom the editor

Ouverture du Vol 8 : la fondation de tout un style.Opening Vol 8: the foundation of an entire style.

Ce numéro pose la première pierre du volume, et c'est la plus profonde. Tout le réflexe fonctionnel de Ruby — map, select, reduce que les deux prochains numéros vont composer puis raffiner — repose sur ce seul fait : un each écrit une fois débloque cinquante transformations. Ce n'est pas une commodité, c'est une philosophie : tu ne décris pas comment faire chaque opération, tu décris comment parcourir, et le langage déduit le reste. C'est le « tout est message » du Vol 1 appliqué aux collections — define le message minimal (each), et toute une famille de messages répond. Le dev venant d'un langage où map, filter, reduce sont des fonctions globales découvre ici qu'en Ruby, ce sont des conséquences d'un contrat qu'il contrôle.This issue lays the volume's first stone, and it's the deepest. Ruby's entire functional reflex — map, select, reduce that the next two issues will compose then refine — rests on this single fact: an each written once unlocks fifty transformations. It's not a convenience, it's a philosophy: you don't describe how to do each operation, you describe how to traverse, and the language deduces the rest. It's Vol 1's 'everything is a message' applied to collections — define the minimal message (each), and a whole family of messages answers. A dev from a language where map, filter, reduce are global functions discovers here that in Ruby, they're consequences of a contract they control.

Prochain numéro : map / select / reduce vs les boucles. On a la fondation ; le № 2 installe le style. Penser en transformations chaînées — map puis select puis reduce — plutôt qu'en accumulation impérative dans une boucle. On verra each_with_object, inject, flat_map, tap et then, et pourquoi une chaîne lisible dit l'intention là où une boucle la cache dans un accumulateur muté.Next issue: map / select / reduce vs loops. We have the foundation; № 2 installs the style. Thinking in chained transformations — map then select then reduce — rather than imperative accumulation in a loop. We'll see each_with_object, inject, flat_map, tap and then, and why a readable chain states the intent where a loop hides it in a mutated accumulator.