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

map / select / reduce, ou penser en transformations map / select / reduce, or thinking in transformations

Une boucle qui accumule cache son intention dans la mécanique ; une chaîne la nomme étape par étape. map transforme, select filtre, reduce replie, each_with_object bâtit. Le piège du style : chaîner pour chaîner — si tu jettes un map, tu voulais each ; si un mot suffit (find, tally), écris-le. A loop that accumulates hides its intent in machinery; a chain names it step by step. map transforms, select filters, reduce folds, each_with_object builds. The style's trap: chaining for its own sake — discard a map and you wanted each; if one word fits (find, tally), write it.

AudienceAudience
Dev qui écrit des boucles et veut penser en pipelines lisibles Dev who writes loops and wants to think in readable pipelines
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●○○ mapreduceChaînage
SommaireContents ·
№ 01CadrageFraming3 min

Décrire une transformation, au lieu de piloter une boucle.Describe a transformation, instead of driving a loop.

La fondation posée (un each débloque tout), voici le style qu'elle permet. Une boucle impérative dit comment : crée un accumulateur, parcours, teste, pousse, additionne. Une chaîne de transformations dit quoi : garde les payées, applique la TVA, fais la somme. Trois verbes couvrent l'essentiel — map transforme chaque élément (1 pour 1), select filtre (garde ou jette), reduce replie une collection en une seule valeur. Les chaîner, c'est écrire un pipeline qui se lit de gauche à droite comme une phrase, où chaque étape nomme son intention au lieu de la cacher dans la mécanique d'une boucle.With the foundation laid (one each unlocks everything), here's the style it enables. An imperative loop says how: create an accumulator, traverse, test, push, add. A chain of transformations says what: keep the paid ones, apply the tax, sum. Three verbs cover the essentials — map transforms each element (1 for 1), select filters (keep or drop), reduce folds a collection into a single value. Chaining them writes a pipeline that reads left to right like a sentence, where each step names its intent instead of hiding it in a loop's machinery.

La même intention : une boucle, ou une chaîneThe same intent: a loop, or a chain
# impératif : on accumule à la main, l'intention se devine
totals = []
orders.each do |o|
  next unless o.paid?
  totals << o.amount * 1.2
end
sum = 0
totals.each { |t| sum += t }

# déclaratif : on DÉCRIT la transformation, l'intention se lit
sum = orders.select(&:paid?)        # garder les payées
            .map { |o| o.amount * 1.2 }  # appliquer la TVA
            .sum                    # replier en un total
Trois verbes, trois formesThree verbs, three shapes
map
● ● ● → ◆ ◆ ◆
1 pour 1 : transforme1 for 1: transform
select
● ● ● → ● ●
filtre : garde ou jettefilter: keep or drop
reduce
● ● ● → ◆
replie : tout en unfold: all into one

Chaque verbe a une forme reconnaissable : map conserve le nombre d'éléments, select le réduit, reduce l'écrase à une valeur. Lire une chaîne, c'est suivre ces formes — la donnée se transforme étape par étape, à vue.Each verb has a recognizable shape: map keeps the element count, select shrinks it, reduce collapses it to one value. Reading a chain is following these shapes — the data transforms step by step, in plain sight.

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

« Cette boucle accumule un résultat — quelle transformation décrit-elle ? » Si tu crées un tableau ou un total vide, que tu parcours et que tu pousses dedans, tu écris à la main un map, un select ou un reduce. Nomme-le : la chaîne dira l'intention que la boucle cachait."This loop accumulates a result — which transformation does it describe?" If you create an empty array or total, traverse, and push into it, you're hand-writing a map, a select or a reduce. Name it: the chain will state the intent the loop hid.

Le style n'est pas qu'esthétique.The style isn't just aesthetics.

Une chaîne se lit, se relit et se modifie par étape : insérer un filtre, c'est ajouter un .select au bon endroit, sans toucher au reste. Une boucle impérative mêle parcours, condition et accumulation dans un même bloc — pour la changer, il faut tout relire. La transformation chaînée sépare les préoccupations que la boucle entremêle. C'est de la lisibilité, mais aussi de la modifiabilité.A chain reads, re-reads and changes step by step: inserting a filter means adding a .select at the right spot, untouched rest. An imperative loop mixes traversal, condition and accumulation in one block — to change it, you reread it all. The chained transformation separates the concerns the loop entangles. It's readability, but also modifiability.

№ 02La surpriseThe surprise4 min

La boucle cache l'intention dans un accumulateur ; la chaîne la montre.The loop hides the intent in an accumulator; the chain shows it.

Mets les deux versions côte à côte et la différence saute aux yeux. La boucle impérative entrelace trois choses dans un seul bloc : le filtre (next unless), l'initialisation de l'accumulateur (||= 0) et le repli (+=). Pour la comprendre, on doit la dérouler mentalement, tour par tour, et reconstituer ce qu'elle calcule. La chaîne sépare ces préoccupations en étapes nommées, lues de haut en bas : garder, regrouper, sommer. On ne simule plus l'exécution ; on lit une description. C'est ça, penser en transformations : décrire le quoi, et laisser chaque méthode porter le comment.Put the two versions side by side and the difference is obvious. The imperative loop interleaves three things in one block: the filter (next unless), the accumulator init (||= 0) and the fold (+=). To understand it, you must unroll it mentally, turn by turn, and reconstruct what it computes. The chain separates these concerns into named steps, read top to bottom: keep, group, sum. You no longer simulate execution; you read a description. That's thinking in transformations: describe the what, and let each method carry the how.

Filtre, init, repli — entremêlésFilter, init, fold — entangled
# l'intention est noyée dans la mécanique d'accumulation
def report(orders)
  by_country = {}
  orders.each do |o|
    next unless o.paid?              # le filtre, perdu au milieu
    by_country[o.country] ||= 0      # l'init manuelle de l'accumulateur
    by_country[o.country] += o.total # le repli, à la main
  end
  by_country
end
# pour comprendre : il faut SIMULER la boucle dans sa tête, tour par tour

Rien n'est faux ici — mais tout est mêlé. Le next unless, le ||= 0, le += vivent dans le même bloc, et l'intention (« total payé par pays ») n'apparaît nulle part en clair : il faut la déduire de la mécanique. Ajouter un critère, c'est rouvrir et relire toute la boucle.Nothing is wrong here — but everything is mixed. The next unless, the ||= 0, the += live in one block, and the intent ("paid total per country") appears nowhere plainly: you deduce it from the machinery. Adding a criterion means reopening and rereading the whole loop.

Une boucle te fait simuler l'exécution ; une chaîne te fait lire l'intention. La seconde se modifie une étape à la fois.A loop makes you simulate execution; a chain makes you read the intent. The latter changes one step at a time.
№ 03La mécaniqueThe mechanics5 min

Au-delà du trio : each_with_object, flat_map, tap, then.Beyond the trio: each_with_object, flat_map, tap, then.

map, select, reduce couvrent l'essentiel, mais quelques voisins rendent les chaînes plus nettes. flat_map fait map puis aplatit d'un niveau ; filter_map combine select et map en une passe. Pour construire une collection, each_with_object bat souvent inject : il rend l'objet automatiquement, là où inject t'oblige à renvoyer l'accumulateur à chaque tour (l'oublier est un bug classique). Et deux outils de chaînage pur : tap espionne une valeur sans rompre la chaîne (il renvoie self), then (alias yield_self) passe une valeur à un bloc et renvoie son résultat — un pipe pour brancher une étape qui n'est pas une méthode de la collection.map, select, reduce cover the essentials, but a few neighbors make chains cleaner. flat_map does map then flattens one level; filter_map combines select and map in one pass. To build a collection, each_with_object often beats inject: it returns the object automatically, where inject forces you to return the accumulator each turn (forgetting it is a classic bug). And two pure-chaining tools: tap spies a value without breaking the chain (it returns self), then (alias yield_self) passes a value to a block and returns its result — a pipe to plug in a step that isn't a collection method.

Le trio, et ses raccourcis flat_map / filter_mapThe trio, and its flat_map / filter_map shortcuts
[1, 2, 3].map { |x| x * 2 }            # => [2, 4, 6]  1 pour 1
[1, 2, 3, 4].select(&:even?)          # => [2, 4]     filtre
[1, 2, 3, 4].reduce(0) { |a, x| a + x } # => 10       replie (ou .sum)

# flat_map : map + aplatit d'un niveau (évite un .map.flatten)
[[1, 2], [3, 4]].flat_map { |pair| pair }   # => [1, 2, 3, 4]

# filter_map : select + map en une passe (évite un .map.compact)
[1, 2, 3, 4].filter_map { |x| x * 10 if x.even? }   # => [20, 40]
Construire, espionner, brancherBuild, spy, plug in
# each_with_object : construire une collection SANS threader le retour
words.each_with_object(Hash.new(0)) { |w, counts| counts[w] += 1 }
# l'objet (counts) est rendu automatiquement — pas de « ; counts » final

# inject/reduce fait pareil, mais TU dois renvoyer l'accumulateur :
words.inject(Hash.new(0)) { |counts, w| counts[w] += 1; counts }
#                                                          ^^^^^ oublié = bug

# tap : espionne sans casser la chaîne (renvoie self)
build_query.tap { |q| log(q) }.execute

# then (yield_self) : passe self à un bloc et renvoie son résultat
total.then { |t| Money.new(t) }
each_with_object vs inject.each_with_object vs inject.

Pour réduire en une valeur (somme, max), reduce/inject est parfait. Pour construire une collection (un hash de comptes, un tableau groupé), each_with_object est plus sûr : l'objet accumulateur est rendu pour toi, tu ne peux pas oublier le ; counts final qui fait planter inject en silence. Et tap/then branchent ce que la collection ne sait pas faire : journaliser au milieu d'une chaîne (tap), ou passer le résultat à un constructeur (then), sans casser le fil de lecture.To reduce to one value (sum, max), reduce/inject is perfect. To build a collection (a counts hash, a grouped array), each_with_object is safer: the accumulator object is returned for you, you can't forget the trailing ; counts that silently breaks inject. And tap/then plug in what the collection can't do: log mid-chain (tap), or pass the result to a constructor (then), without breaking the reading thread.

Le repère de choix : map quand le résultat fait 1 pour 1, select/reject pour filtrer, reduce pour replier en une valeur, each_with_object pour bâtir une collection. Et avant d'enchaîner deux étapes, regarde s'il existe une méthode unique qui le dit — flat_map, filter_map, sum, tally, partition — c'est le sujet du piège qui suit.The choosing cue: map when the result is 1 for 1, select/reject to filter, reduce to fold into one value, each_with_object to build a collection. And before chaining two steps, check whether a single method says it — flat_map, filter_map, sum, tally, partition — the subject of the trap that follows.

№ 04Le piègeThe trap4 min

Chaîner pour chaîner : map pour un effet, ou un pipeline qu'un mot disait.Chaining for its own sake: map for an effect, or a pipeline one word said.

Une fois converti aux transformations, on sur-corrige : on enchaîne par réflexe, même là où c'est trompeur ou inutile. Deux formes. La première : map pour un effet de bord — on transforme pour envoyer des mails, en ignorant le tableau produit. map alloue ce tableau pour rien, et surtout il ment au lecteur : il annonce « je calcule de nouvelles valeurs » alors qu'on voulait juste « pour chacun, fais ». each dit l'intention exacte. La seconde : une chaîne ingénieuse — select.first, map.compact, group_by.transform_values — là où Ruby a une méthode unique : find, filter_map, tally. Le réflexe fonctionnel n'est pas « tout chaîner », c'est « nommer juste » : each pour l'effet, et la méthode dédiée plutôt qu'un pipeline malin.Once converted to transformations, you over-correct: you chain by reflex, even where it's misleading or pointless. Two forms. First: map for a side effect — you transform to send emails, ignoring the produced array. map allocates that array for nothing, and worse it lies to the reader: it announces 'I'm computing new values' when you just meant 'for each, do'. each states the exact intent. Second: a clever chain — select.first, map.compact, group_by.transform_values — where Ruby has a single method: find, filter_map, tally. The functional reflex isn't 'chain everything', it's 'name precisely': each for the effect, and the dedicated method over a clever pipeline.

map jeté, et des chaînes qu'un mot remplacediscarded map, and chains one word replaces
# Piège A : map pour un EFFET DE BORD — le tableau de retour est jeté
users.map { |u| send_welcome_email(u) }   # on ignore la valeur produite
# map alloue un Array de retours inutile, et SIGNALE « transformation »
# au lecteur, qui cherche en vain ce qu'on en fait

# Piège B : une chaîne maligne là où UNE méthode existe
orders.select(&:paid?).first               # → orders.find(&:paid?)
list.map(&:to_i).compact                   # → list.filter_map { ... }
words.group_by(&:itself).transform_values(&:size)  # → words.tally
each pour l'effet, le mot juste pour le resteeach for the effect, the right word for the rest
# A : each dit l'intention — itérer pour l'effet, rien à collecter
users.each { |u| send_welcome_email(u) }

# B : la méthode dédiée dit le quoi, en un mot
orders.find(&:paid?)         # le premier payé, et il s'arrête là
list.filter_map { |s| Integer(s, exception: false) }   # filtre + transforme
words.tally                  # => { "a" => 3, "b" => 1 }  comptage direct
Le contre-exemple obligatoire.The mandatory counter-example.

Deux questions avant d'enchaîner. Un : est-ce que j'utilise le résultat ? Si non, ce n'est pas une transformation — c'est each. map dont on jette le retour trompe le lecteur et alloue pour rien. Deux : existe-t-il une méthode pour ça ? find bat select.first (et s'arrête au premier), filter_map bat map.compact, tally bat group_by+transform_values, sum/min_by/partition battent un reduce à la main. Penser en transformations, c'est choisir le verbe exact, pas empiler des maillons.Two questions before chaining. One: do I use the result? If not, it's not a transformation — it's each. A map whose return you discard misleads the reader and allocates for nothing. Two: is there a method for this? find beats select.first (and stops at the first), filter_map beats map.compact, tally beats group_by+transform_values, sum/min_by/partition beat a hand-rolled reduce. Thinking in transformations is choosing the exact verb, not stacking links.

Si tu jettes le résultat d'un map, tu voulais each. Si une chaîne tient en un mot du langage, écris le mot.If you discard a map's result, you wanted each. If a chain fits one word of the language, write the word.
№ 05Bilan & éditoWrap-up & editorial2 min

Décris la transformation — puis choisis le verbe exact.Describe the transformation — then choose the exact verb.

Une boucle qui accumule cache son intention dans la mécanique ; une chaîne la nomme étape par étape. map transforme 1 pour 1, select filtre, reduce replie en une valeur, each_with_object bâtit une collection. Autour, des raccourcis nets : flat_map, filter_map, tally, et tap/then pour brancher ou espionner sans rompre le fil. Mais le style a son propre piège : chaîner pour chaîner. Si tu jettes le résultat d'un map, tu voulais each ; si un pipeline tient en un mot du langage, écris le mot.A loop that accumulates hides its intent in machinery; a chain names it step by step. map transforms 1 for 1, select filters, reduce folds to one value, each_with_object builds a collection. Around them, crisp shortcuts: flat_map, filter_map, tally, and tap/then to plug in or spy without breaking the thread. But the style has its own trap: chaining for its own sake. If you discard a map's result, you wanted each; if a pipeline fits one word of the language, write the word.

L'intentionThe intentLe verbeThe verbLe réflexeThe reflex
Transformer 1 pour 1Transform 1 for 1map — et son résultat est utilisé.map — and its result is used.Résultat jeté ? c'était each.Result discarded? it was each.
Garder / écarterKeep / dropselect / reject, ou filter_map.select / reject, or filter_map.select.first → find ; map.compact → filter_map.select.first → find; map.compact → filter_map.
Replier en une valeurFold to one valuereduce/sum/min_by… selon le cas.reduce/sum/min_by… as fits.Une méthode dédiée bat un reduce maison.A dedicated method beats a hand-rolled reduce.
Bâtir une collectionBuild a collectioneach_with_object (l'objet est rendu).each_with_object (the object is returned).Plus sûr qu'inject : pas d'accumulateur à renvoyer.Safer than inject: no accumulator to return.
Penser en transformations, ce n'est pas tout chaîner — c'est nommer chaque étape par le verbe juste, et s'arrêter dès qu'un mot suffit.Thinking in transformations isn't chaining everything — it's naming each step with the right verb, and stopping the moment one word suffices.
Mot de l'éditeurFrom the editor

Une chaîne lisible est un raisonnement rendu visible.A readable chain is reasoning made visible.

Le passage de la boucle à la chaîne n'est pas un tic de style : c'est un changement de niveau de discours. La boucle décrit une procédure — fais ceci, puis cela, tour par tour. La chaîne décrit une intention — voici la suite de transformations qui mène de la donnée brute au résultat. Le second se lit, se vérifie et se modifie sans l'exécuter dans sa tête. Mais ce numéro tient aussi à son garde-fou : le but n'est pas l'élégance pour elle-même. Une chaîne qui transforme pour rien, ou qui réinvente find en trois maillons, n'est pas plus lisible — elle est juste à la mode. Le bon réflexe fonctionnel reste sobre : le verbe exact, et pas un de plus.Moving from loop to chain isn't a stylistic tic: it's a change in the level of discourse. The loop describes a procedure — do this, then that, turn by turn. The chain describes an intent — here's the sequence of transformations from raw data to result. The latter reads, verifies and changes without running it in your head. But this issue holds to its guardrail too: the goal isn't elegance for its own sake. A chain that transforms for nothing, or reinvents find in three links, isn't more readable — it's just fashionable. The good functional reflex stays sober: the exact verb, and not one more.

Prochain numéro : lazy & les Enumerator. On a la fondation et le style ; le № 3 apporte le raffinement. Chaque maillon d'une chaîne alloue un tableau intermédiaire — sur de gros volumes, ou des séquences infinies, c'est coûteux ou impossible. lazy diffère le calcul et ne produit que ce qu'on demande : (1..Float::INFINITY).lazy.map{…}.first(5) ne calcule que cinq éléments. On verra quand ce raffinement paie, et quand son overhead coûte plus que le gain.Next issue: lazy & Enumerators. We have the foundation and the style; № 3 brings the refinement. Each link of a chain allocates an intermediate array — on large volumes, or infinite sequences, that's costly or impossible. lazy defers the work and produces only what's asked: (1..Float::INFINITY).lazy.map{…}.first(5) computes only five elements. We'll see when this refinement pays, and when its overhead costs more than the gain.