skip to content

In Ruby, what changes when define_method receives a Proc, a lambda or an UnboundMethod as the body instead of a block?

level: middleimportance: should knowfreq 30%

answer

  1. four accepted body types
  2. every Proc becomes lambda-like
  3. strict arity, return exits method
  4. self is the receiver
  5. bind argument must be a subclass

basics

~20 s

A Proc or lambda body is copied and given lambda semantics, so arity is strict and return leaves the method. An UnboundMethod body reuses an existing definition, but only from the same class, an ancestor or a module; otherwise TypeError.

solid answer

~40 s

`define_method` accepts a block, a Proc, a lambda, a `Method` or an `UnboundMethod`. Any Proc body is duplicated and marked as a lambda, so even a plain `proc { |a, b| }` becomes strict: calling with one argument raises `ArgumentError`, and `return` inside it just returns from the new method. The body runs with `self` set to the receiver, while its captured locals stay those of the scope where it was created. A `Method` or `UnboundMethod` body reuses that method's definition under the new name, which is how you copy a method, but the source must be owned by the target class, an ancestor or a module; an unrelated class gives `TypeError` ("bind argument must be a subclass of ...").

code

ruby · 9 lines
ruby
class Report; def title = "Q3"; end
class Chart; end

Chart.define_method(:title, Report.instance_method(:title))
# TypeError: bind argument must be a subclass of Report

module Titled; def title = "untitled"; end
Chart.define_method(:title, Titled.instance_method(:title))
Chart.new.title  # => "untitled"

go deeper

for a junior

Recall that define_method takes a block or a second argument that is a Proc, lambda, Method or UnboundMethod.

for a middle

Explain the lambda conversion, strict arity and return, the rebinding of self, and the ownership rule for UnboundMethod bodies.

for a senior

Show when copying a definition with an UnboundMethod beats a wrapper block, and what TypeError tells you about a bad source.

for a principal

Judge whether generated bodies built from shared Procs keep a codebase understandable, or whether plain methods would serve better.

## What define_method accepts `Module#define_method` takes the new method's name and a **body**. The body can arrive in four forms: - a **block**: `define_method(:x) { ... }`; - a **Proc** object, including one made by `proc` or `Proc.new`: `define_method(:x, some_proc)`; - a **lambda**: `define_method(:x, ->(a) { ... })`; - a **Method** or **UnboundMethod** object: `define_method(:x, instance_method(:y))`. Anything else raises `TypeError` ("wrong argument type Integer (expected Proc/Method/UnboundMethod)"). With neither a block nor a second argument, Ruby raises `ArgumentError` ("tried to create Proc object without a block"). ## Blocks and Procs become lambdas When the body is a block or a Proc whose code is a Ruby block (made by `proc`, `Proc.new` or `lambda`), CRuby **duplicates** it and marks the copy as a lambda before storing it. Two consequences follow, whether you passed a proc or a lambda: 1. **Arity is strict.** A plain `proc { |a, b| }` would normally accept one argument and set `b` to `nil`. As a method body it rejects the call with `ArgumentError` ("wrong number of arguments (given 1, expected 2)"). 2. **`return` leaves the method.** Inside an ordinary proc, `return` tries to return from the method where the proc was created. As a method body, `return 5` simply makes the method return `5`. The original Proc object you passed is not changed; only the stored copy is. A third point surprises people: **`self` is rebound.** The body runs with `self` set to the receiver of the new method, even if the Proc was created somewhere else. Instance variables and bare method calls inside it refer to the object the method was called on, not to the place the Proc came from. Local variables the Proc captured are still the ones from its original scope. ## Method and UnboundMethod bodies A `Method` or `UnboundMethod` body reuses an existing method's definition under a new name. The typical case is copying a method, for example to keep the original around before redefining it: ```ruby class Invoice def total = 100 define_method(:original_total, instance_method(:total)) def total = original_total + 20 end Invoice.new.total # => 120 ``` The rule is about **ownership**. The method must belong to the class you are defining into, to one of its ancestors, or to a **module**: | Source of the body | Target class | Result | |---|---|---| | `Invoice.instance_method(:total)` | `Invoice` or a subclass | works | | an instance method of a module | any class | works | | `Invoice.instance_method(:total)` | an unrelated class | `TypeError`: bind argument must be a subclass of Invoice | | a singleton method | another class | `TypeError`: can't bind singleton method to a different class | Module methods are allowed anywhere because a module's method makes no assumption about the class it lands in. ## Choosing a form - Use a **block** for almost everything: it is the most readable and captures the local values you need. - Pass a **lambda or Proc** when the same body is built once and reused for several names, or comes from a factory method. - Pass an **UnboundMethod** when you want an existing method's exact implementation, not a wrapper that calls it; a wrapper would add a frame and a closure. ## A note on Method bodies and receivers A `Method` object (from `obj.method(:x)`) is accepted too, but only its definition is used; the receiver it was bound to is discarded, and the new method runs on whatever object it is later called on. The ownership rule is the same as for an `UnboundMethod`. ## A compact demonstration ```ruby class Pair define_method(:both, proc { |a, b| [a, b] }) define_method(:early, proc { return :out; :never }) end Pair.new.early # => :out Pair.new.both(1) # ArgumentError: wrong number of arguments (given 1, expected 2) Pair.instance_method(:both).arity # => 2 ``` ## What an interviewer is listening for The key sentence is: every Proc body is converted to lambda semantics, so arity is strict and `return` returns from the method, while an UnboundMethod body must come from the same class, an ancestor or a module. A strong answer adds that `self` inside the body is the receiver, not the Proc's original `self`.

  • If a lambda built elsewhere reads @name, whose @name does the generated method see?
    The receiver's. `define_method` runs the body with `self` set to the object the method was called on, so `@name` and bare method calls resolve against that object. Only captured local variables keep referring to the scope where the lambda was created.
  • Why pass an UnboundMethod rather than a block that calls the original method?
    An UnboundMethod body is the original definition itself, so there is no extra frame and no closure. A wrapper block adds a call per invocation and captures its surrounding scope. The UnboundMethod form also keeps working if the original name is later redefined, because it holds the old definition, not the name.

saying these in an interview costs you the question

  • a proc passed to define_method keeps its lenient arity
  • return inside a proc body returns from where the proc was created
  • any UnboundMethod can be used as a body for any class
  • define_method mutates the Proc you pass into a lambda
  • self inside a passed lambda stays the lambda's original self