Vol. 6 · № 04 · Un atelier RubyA Ruby workshop

L'objet Method, ou pourquoi on ne le passe pas The Method object, and why you don't pass it

Définir une méthode ne donne aucune valeur à transporter : il faut la demander, obj.method(:m), pour obtenir un objet Method appelable comme une lambda. Mais ce handle est un instantané — il fige l'implémentation au moment de la capture. Envoyer le message, lui, ré-résout à chaque appel. Toute la différence entre une méthode et un message tient là. Defining a method gives you no value to carry: you have to ask for it, obj.method(:m), to get a Method object callable like a lambda. But that handle is a snapshot — it freezes the implementation at capture time. Sending the message instead re-resolves on every call. The whole difference between a method and a message lives there.

AudienceAudience
Dev à l'aise avec procs/lambdas, prêt à réifier une méthode Dev comfortable with procs/lambdas, ready to reify a method
Format
Self-paced
ChapitresChapters
5
Date
Mai 2026 May 2026
≈ 21 min ●●●○ MethodMessageLate binding
SommaireContents ·
№ 01CadrageFraming3 min

Une méthode n'est pas une valeur — il faut la demander.A method isn't a value — you have to ask for one.

Le Vol 6 manipule classes et méthodes à l'exécution ; ce numéro part de la plus discrète de ces manipulations. Contrairement à JavaScript ou Python, en Ruby une méthode n'est pas un objet de première classe : la définir ne te rend aucune valeur transportable, et écrire son nom seul lève une NameError. Tu peux pourtant réifier une méthode — en demander un objet, via obj.method(:nom). Tu obtiens un Method, qu'on appelle avec .call exactement comme un proc ou une lambda. Mais pourquoi cette étape de plus ? Pourquoi les méthodes ne sont-elles pas first-class d'office ? Parce que Ruby, langage purement objet dans la lignée de Smalltalk, sépare une chose qu'on confond presque toujours : la méthode et le message.Vol 6 manipulates classes and methods at runtime; this issue starts from the quietest of those manipulations. Unlike JavaScript or Python, in Ruby a method is not a first-class object: defining it hands you no value to carry, and writing its name alone raises a NameError. You can still reify a method — ask for an object representing it, via obj.method(:name). You get a Method, called with .call exactly like a proc or a lambda. But why this extra step? Why aren't methods first-class out of the box? Because Ruby, a purely object-oriented language in the Smalltalk lineage, separates a thing we almost always conflate: the method and the message.

Définir ne donne rien à passer ; réifier, siDefining gives nothing to pass; reifying does
class Greeter
  def hello(name) = "Bonjour, #{name}"
end

g = Greeter.new
g.hello("Ada")          # => "Bonjour, Ada"   un message hello envoyé à g
hello                   # NameError : aucune valeur libre nommée hello
                        #   ↑ définir une méthode ne crée AUCUN objet à passer

m = g.method(:hello)    # on DEMANDE un objet qui représente la méthode
m.class                 # => Method
m.call("Ada")           # => "Bonjour, Ada"   appelable comme un proc/lambda
m.owner                 # => Greeter          d'où vient l'implémentation
message vs méthodemessage vs method
envoyersend
le message :hellothe :hello message
›
Greeter
›
chaîne d'ancêtresancestor chain
›
une méthode — à chaque envoia method — on every send
réifierreify
g.method(:hello)
›
la même chaîne, une seule foissame chain, exactly once
›
un Method — résultat emportéa Method — result carried away

Un message est le nom d'une responsabilité ; une méthode est un bout de code concret qui en réalise une version. Envoyer g.hello, c'est laisser g chercher, maintenant, quelle méthode répond. Demander g.method(:hello), c'est figer cette réponse dans un objet.A message is the name of a responsibility; a method is a concrete piece of code realizing one version of it. Sending g.hello lets g look up, now, which method answers. Asking g.method(:hello) freezes that answer into an object.

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

« Pour faire circuler un comportement, je passe l'objet et j'envoie le message — pas un obj.method(:m). » L'objet Method a ses usages, mais c'est un instantané : on verra qu'il fige l'implémentation au moment de la capture, alors qu'un message est ré-résolu à chaque envoi."To pass a behavior around, I hand over the object and send the message — not an obj.method(:m)." The Method object has its uses, but it's a snapshot: we'll see it freezes the implementation at capture time, whereas a message is re-resolved on every send.

Pourquoi pas first-class ?Why not first-class?

Le choix de ne pas rendre les méthodes first-class d'office est cohérent avec la nature de Ruby, langage purement objet à la Smalltalk. Alan Kay voyait les objets comme des cellules indépendantes qui s'envoient des messages, sans présumer de la manière dont ils seront traités. Passer des références à des méthodes précises, c'est court-circuiter cette indirection — et s'exposer à ce que la référence se périme. Tout le numéro déplie ce coût.The choice not to make methods first-class out of the box is consistent with Ruby's nature as a purely object-oriented, Smalltalk-style language. Alan Kay saw objects as independent cells sending each other messages, with no assumptions about how they'd be handled. Passing references to specific methods short-circuits that indirection — and risks the reference going stale. The whole issue unpacks that price.

№ 02La surpriseThe surprise4 min

Le handle ressemble à un lien vivant — c'est une photo.The handle looks like a live link — it's a photograph.

On croit qu'un objet Method est une poignée branchée en permanence sur l'objet, qui suivrait ses évolutions. Faux : il fige l'implémentation résolue à l'instant de la capture. Enrichis l'objet juste après — un extend, un module qui redéfinit notify — et le handle, lui, ne bouge pas : il appelle toujours l'ancienne méthode, son owner reste UI. Envoie au contraire le message notify à l'objet, et tu obtiens à chaque fois la version la plus à jour, parce que l'envoi de message refait la résolution le long de la chaîne d'ancêtres. Le même appel, deux temporalités : la photo et la question reposée.You'd think a Method object is a handle permanently wired into the object, tracking its changes. Wrong: it freezes the implementation resolved at the instant of capture. Enrich the object right after — an extend, a module redefining notify — and the handle doesn't budge: it still calls the old method, its owner stays UI. Send the notify message to the object instead, and every time you get the most up-to-date version, because a message send redoes the lookup along the ancestor chain. The same call, two timelines: the photo and the question re-asked.

Capturé avant extend : aucune mise à jourCaptured before extend: no update
module Bell
  def notify(msg)
    super
    puts "beep"
  end
end

class UI
  def notify(msg) = puts(msg)
end

ui = UI.new
saved = ui.method(:notify)   # capture l'implémentation ACTUELLE
ui.extend(Bell)              # on enrichit l'objet APRÈS la capture

saved.call("thé prêt")       # "thé prêt"            ← pas de beep : figé
saved.owner                  # => UI    le handle pointe l'ancienne méthode

saved a été créé avant le extend : il a figé UI#notify, et rien ne le rattachera à Bell#notify. Son owner le trahit — il reste UI. C'est exactement le genre de référence qui « se périme » entre le moment où on la stocke et le moment où on l'exécute.saved was created before the extend: it froze UI#notify, and nothing will rebind it to Bell#notify. Its owner gives it away — it stays UI. This is exactly the kind of reference that "goes stale" between the moment you store it and the moment you run it.

Un objet Method fige une réponse ; un message repose la question — et l'objet répond avec ce qu'il sait aujourd'hui.A Method object freezes an answer; a message re-asks the question — and the object answers with what it knows today.
№ 03La mécaniqueThe mechanics6 min

Le Method résout une fois ; le message résout à chaque envoi.A Method resolves once; a message resolves on every send.

obj.method(:m) renvoie un Method qui mémorise deux choses : son receveur (l'objet), et son owner (la classe ou le module exact d'où vient le code, résolu une seule fois le long de la chaîne d'ancêtres). De là, unbind donne une UnboundMethod détachable, et to_proc un Proc — c'est pourquoi &obj.method(:m) se passe en bloc. Mais ce confort cache le mécanisme : la capture a fait la recherche une fois pour toutes. Un envoi de message, lui, refait cette recherche à chaque appel — c'est le late binding. Conséquence directe : réécris la méthode, extend ou prepend l'objet après la capture, et le message suit pendant que le handle reste figé.obj.method(:m) returns a Method that remembers two things: its receiver (the object), and its owner (the exact class or module the code comes from, resolved once along the ancestor chain). From there, unbind gives a detachable UnboundMethod, and to_proc a Proc — which is why &obj.method(:m) passes as a block. But that convenience hides the mechanism: capture did the lookup once and for all. A message send instead redoes that lookup on every call — that's late binding. Direct consequence: rewrite the method, extend or prepend the object after capture, and the message follows while the handle stays frozen.

L'anatomie d'un Method : receveur + owner, figésA Method's anatomy: receiver + owner, frozen
m = g.method(:hello)   # Method : LIÉ à g, RÉSOLU vers Greeter#hello
m.receiver             # => g          le destinataire fixé
m.owner                # => Greeter    la classe d'où sort l'implémentation
m.unbind               # => UnboundMethod : la méthode sans receveur
m.to_proc              # => Proc       d'où &g.method(:hello) marche en bloc

["Ada", "Bo"].map(&g.method(:hello))   # capture figée passée en bloc
["Ada", "Bo"].map { |n| g.hello(n) }   # message ré-envoyé à chaque tour
carte d'identitéidentity card
receiver
g
le destinataire, fixéthe receiver, pinned
owner
Greeter
d'où sort le code, résoluwhere the code lives, resolved
to_proc
Proc
passable en blocblock-passable
unbind
UnboundMethod
la méthode sans receveurthe method, receiver-less
résolu une fois — à la captureresolved once — at capture
Late binding : le message gagne la course au tempsLate binding: the message wins the race with time
g = Greeter.new
m = g.method(:hello)   # on fige la résolution maintenant

class Greeter          # classes ouvertes (Vol 6 № 3) : on réécrit la méthode
  def hello(name) = "Salut, #{name}"
end

g.hello("Ada")   # => "Salut, Ada"     le message voit la nouvelle définition
m.call("Ada")    # => "Bonjour, Ada"   le handle a figé l'ancienne
Une seule résolution, gravée.A single resolution, etched in.

Le Method capturé pointe l'implémentation telle qu'elle était à la capture ; la réécrire ensuite (classes ouvertes), extend ou prepend un module, n'y change rien. Le message, lui, reconsulte la chaîne d'ancêtres du Vol 5 à chaque envoi — il voit toujours la dernière définition. Le handle est utile pour justement figer un instantané ; ailleurs, il introduit un couplage temporel silencieux entre le moment où on le crée et celui où on l'exécute.The captured Method points at the implementation as it was at capture; rewriting it later (open classes), extend-ing or prepend-ing a module, changes nothing. The message re-consults Vol 5's ancestor chain on every send — it always sees the latest definition. The handle is useful precisely when you want to freeze a snapshot; elsewhere it introduces a silent temporal coupling between when you create it and when you run it.

&:hello n'est pas &m&:hello is not &m

list.map(&:hello) convertit le symbole en proc qui envoie le message hello à chaque élément : late binding, receveur variable. list.map(&g.method(:hello)) passe une capture figée, receveur fixé. La forme la plus courte est aussi la plus vivante — toute la thèse du numéro tient dans ce &.list.map(&:hello) converts the symbol into a proc that sends the hello message to each element: late binding, variable receiver. list.map(&g.method(:hello)) passes a frozen capture, receiver pinned. The shortest form is also the most alive — the whole issue's thesis fits in that &.

Quand l'instantané est le but : wrapper une méthode qu'on va réécrireWhen the snapshot is the goal: wrapping a method you're about to rewrite
class UI
  original = instance_method(:notify)  # UnboundMethod : on fige EXPRÈS

  define_method(:notify) do |msg|      # № 2 — define_method ferme sur original
    puts "[log] notify"
    original.bind(self).call(msg)      # l'ancienne implémentation, préservée
  end
end

UI.new.notify("thé prêt")   # "[log] notify" puis "thé prêt"
# le message ne peut PLUS atteindre l'ancienne version :
# seul le handle capturé l'a gardée — l'instantané était le but

Voilà l'usage légitime que promettait le cadrage — et il convoque tout le Vol 6. On capture l'originale précisément parce qu'on va la rendre inatteignable : la redéfinition (classes ouvertes, № 3) va masquer UI#notify, et define_method (№ 2) referme sa closure sur le handle qui, seul, la préserve. C'est la mécanique des espions de test et de l'instrumentation : geler une réponse n'est plus un accident temporel, c'est le contrat.Here is the legitimate use the framing promised — and it summons the whole of Vol 6. You capture the original precisely because you're about to make it unreachable: the redefinition (open classes, № 3) will shadow UI#notify, and define_method (№ 2) closes over the handle that alone preserves it. That's the mechanics of test spies and instrumentation: freezing an answer is no longer a temporal accident, it's the contract.

Dernier révélateur de la frontière : obj.method(:m) exige une méthode résolue. Sur une méthode fantôme servie par method_missing (№ 1), il lève NameError tant que respond_to_missing? ne l'annonce pas — le message, lui, se contente d'une responsabilité. Retiens donc : un Method object est le bon outil quand tu veux précisément un instantané — une réponse gelée, indépendante de l'évolution future de l'objet. Dès que tu veux le comportement courant d'un collaborateur, passe l'objet et envoie-lui le message. Le chapitre suivant montre ce qui arrive quand on confond les deux.One last marker of the boundary: obj.method(:m) demands a resolved method. On a ghost method served by method_missing (№ 1), it raises NameError until respond_to_missing? declares it — the message, meanwhile, settles for a responsibility. So keep this: a Method object is the right tool when you specifically want a snapshot — a frozen answer, independent of the object's future evolution. The moment you want a collaborator's current behavior, pass the object and send it the message. The next chapter shows what happens when you confuse the two.

№ 04Le piègeThe trap5 min

Câbler un collaborateur par obj.method(:m) : le beep disparaît.Wiring a collaborator via obj.method(:m): the beep vanishes.

Le contre-exemple obligatoire du numéro, tiré d'une vraie injection de dépendances. Une TeaClock relie un minuteur à une UI : le minuteur veut juste un objet à prévenir quand le thé est prêt, sans savoir comment l'alerte est rendue. On lui injecte donc @ui.method(:notify) — un callable, propre en apparence. Puis la clock initialise ses plugins, dont l'un extend l'UI pour ajouter un beep. On lance… et pas de beep. Le minuteur tient un Method capturé AVANT le plugin : il appelle l'ancienne implémentation. C'est un couplage temporel — la référence a été figée trop tôt. La correction : passer l'objet UI lui-même et lui envoyer le message notify. Le minuteur obtient alors le comportement courant, plugin compris.The issue's mandatory counter-example, drawn from real dependency injection. A TeaClock wires a timer to a UI: the timer just wants an object to alert when the tea is ready, without knowing how the alert is rendered. So we inject @ui.method(:notify) — a callable, clean on the surface. Then the clock initializes its plugins, one of which extends the UI to add a beep. We run it… and no beep. The timer holds a Method captured BEFORE the plugin: it calls the old implementation. That's a temporal coupling — the reference was frozen too early. The fix: pass the UI object itself and send it the notify message. The timer then gets the current behavior, plugin included.

Handle capturé trop tôt : le plugin est ignoréHandle captured too early: the plugin is ignored
# Bell : le module « beep » du ch. 02 — StdoutUI : l'équivalent de son UI

class SleepTimer            # le rôle « minuteur » : attendre, puis alerter
  def initialize(seconds, notifier)
    @seconds, @notifier = seconds, notifier
  end
  def start
    sleep(@seconds)
    @notifier.call("Le thé est prêt")     # appelle un callable FIGÉ
  end
end

module Plugins
  class BellPlugin
    def initialize(clock) = clock.ui.extend(Bell)   # enrichit l'UI au démarrage
  end
end

class TeaClock
  attr_reader :ui
  def initialize(seconds)
    @ui = StdoutUI.new
    @timer = SleepTimer.new(seconds, @ui.method(:notify))  # capture AVANT plugins
    Plugins.constants.each { |c| Plugins.const_get(c).new(self) }
  end
  def start = @timer.start
end

TeaClock.new(0.5).start    # "Le thé est prêt"   ← où est passé le beep ?
Passer l'objet, envoyer le messagePass the object, send the message
class SleepTimer
  def start
    sleep(@seconds)
    @notifier.notify("Le thé est prêt")   # ENVOIE le message, ré-résolu
  end
end

class TeaClock
  def initialize(seconds)
    @ui = StdoutUI.new
    @timer = SleepTimer.new(seconds, @ui)   # on passe l'OBJET, pas un handle
    Plugins.constants.each { |c| Plugins.const_get(c).new(self) }
  end
  def start = @timer.start
end

TeaClock.new(0.5).start    # "Le thé est prêt" / "beep"   ← le plugin est vu
Le contre-exemple obligatoire.The mandatory counter-example.

Le critère : veux-tu une réponse figée à cet instant, ou le comportement courant du collaborateur ? Ici, c'est le courant — alors on passe l'objet et on envoie notify. @ui.method(:notify) avait gelé l'implémentation avant le extend du plugin : la référence s'était périmée entre le câblage et l'appel. En passant l'objet, le couplage temporel redevient un simple couplage de nom : tant que l'UI répond au message notify, peu importe quand et comment cette responsabilité est remplie.The test: do you want an answer frozen at this instant, or the collaborator's current behavior? Here it's the current — so pass the object and send notify. @ui.method(:notify) had frozen the implementation before the plugin's extend: the reference went stale between wiring and call. By passing the object, the temporal coupling becomes a plain name coupling: as long as the UI answers the notify message, when and how that responsibility is met no longer matters.

deux temporalitéstwo timelines
t₀ · câblaget₀ · wiring
t₁ · pluginst₁ · plugins
t₂ · startt₂ · start
voie du handle — figéhandle lane — frozen
@ui.method(:notify)
résout et grave UI#notifyresolves and etches UI#notify
extend(Bell)
passe inaperçu : le handle ne regarde plus la chaînegoes unnoticed: the handle no longer looks at the chain
.call
UI#notify — pas de beepUI#notify — no beep
voie du message — vivantmessage lane — live
SleepTimer.new(s, @ui)
on passe l'objet : rien n'est résoluthe object is passed: nothing resolved
extend(Bell)
Bell s'insère devant UI dans la chaîneBell slots in front of UI in the chain
.notify
ré-résout : Bell#notify — beepre-resolves: Bell#notify — beep
Stocker une méthode, c'est geler le temps à l'instant du câblage — et rater tout ce que l'objet apprend ensuite.Storing a method freezes time at the wiring instant — and misses all the object learns afterward.
№ 05Bilan & éditoWrap-up & editorial3 min

Passe l'objet, envoie le message — réserve le handle aux instantanés.Pass the object, send the message — keep the handle for snapshots.

En Ruby, une méthode n'est pas first-class : on en demande un objet par obj.method(:m), appelable comme un proc. Mais ce Method fige son receveur et son owner à la capture — c'est une photo, pas un lien vivant. Un envoi de message refait la résolution le long de la chaîne d'ancêtres à chaque appel : c'est le late binding, et c'est ce qui suit l'objet quand il évolue. D'où la règle : pour faire circuler un comportement, passe l'objet et envoie-lui le message ; ne réifie une méthode que lorsque tu veux précisément geler une réponse. Sinon, la référence se périme — et avec elle, tout ce que l'objet apprend après.In Ruby a method isn't first-class: you ask for an object via obj.method(:m), callable like a proc. But that Method freezes its receiver and owner at capture — it's a photo, not a live link. A message send redoes the lookup along the ancestor chain on every call: that's late binding, and it's what follows the object as it evolves. Hence the rule: to pass a behavior around, hand over the object and send it the message; reify a method only when you specifically want to freeze an answer. Otherwise the reference goes stale — and with it, all the object learns afterward.

La questionThe questionLa règleThe ruleLe réflexeThe reflex
Faire circuler un comportementPass a behavior aroundJe passe l'objet, j'envoie le message.I hand over the object, I send the message.Pas un obj.method(:m) qui se périme.Not an obj.method(:m) that goes stale.
obj.method(:m)obj.method(:m)Un Method : receveur + owner, résolus une fois.A Method: receiver + owner, resolved once.Un instantané, pas un lien vivant.A snapshot, not a live link.
L'objet change après captureThe object changes after captureLe handle reste figé ; le message ré-résout.The handle stays frozen; the message re-resolves.extend / prepend / réécriture : le handle ment.extend / prepend / rewrite: the handle lies.
Quand réifier, alors ?When to reify, then?Quand tu VEUX geler une réponse — wrapper d'une méthode qu'on va réécrire, espion de test.When you WANT to freeze an answer — wrapping a method about to be rewritten, test spy.L'instantané est le but, pas l'accident.The snapshot is the goal, not the accident.
Un message est une responsabilité qu'on confie ; une méthode, une seule façon de la remplir — et un handle gèle cette façon-là pour toujours.A message is a responsibility you entrust; a method, one way to meet it — and a handle freezes that one way forever.
Mot de l'éditeurFrom the editor

Le fil rouge, à nu : tout est message.The through-line, laid bare: everything is a message.

Ce numéro clôt le Vol 6 en revenant au cœur de la série. La métaprogrammation nous a appris à manipuler méthodes et classes à l'exécution ; ce dernier geste — réifier une méthode — révèle, par contraste, pourquoi Ruby ne le fait pas d'office. Une méthode figée est une photo ; un message est une question reposée. Alan Kay imaginait les objets comme des cellules indépendantes s'échangeant des messages, sans présumer de la réponse. Ruby tient cette vision : quand tu écris notifier.notify, tu n'appelles pas une fonction, tu envoies un message, et l'objet décide — maintenant — comment y répondre. C'est pourquoi on passe si rarement des références à des méthodes : on confie des responsabilités à des objets, pas des bouts de code à des variables.This issue closes Vol 6 by returning to the series' core. Metaprogramming taught us to manipulate methods and classes at runtime; this last gesture — reifying a method — reveals, by contrast, why Ruby doesn't do it by default. A frozen method is a photo; a message is a question re-asked. Alan Kay imagined objects as independent cells exchanging messages, with no assumption about the reply. Ruby holds to that vision: when you write notifier.notify, you don't call a function, you send a message, and the object decides — now — how to answer. That's why you so rarely pass references to methods around: you entrust responsibilities to objects, not snippets of code to variables.

Prochain volume : Vol 7, les exceptions. On quitte le modèle objet pur pour la gestion d'erreurs — raise, rescue, ensure — mais le fil tient : une exception aussi est un objet, et la lever, une manière de dire « cette responsabilité, je ne peux pas la remplir ». On verra comment Ruby fait remonter ce message le long de la pile, et pourquoi une hiérarchie d'erreurs bien pensée vaut mieux qu'un rescue attrape-tout.Next volume: Vol 7, exceptions. We leave the pure object model for error handling — raise, rescue, ensure — but the thread holds: an exception too is an object, and raising it is a way of saying 'this responsibility, I cannot meet it'. We'll see how Ruby propagates that message up the stack, and why a well-designed error hierarchy beats a catch-all rescue.