skip to content

In Ruby, when is passing a Method object such as method(:format_row) to a report generator better than passing a block, and when worse?

level: middleimportance: must knowfreq 48%

answer

  1. reuse a named, tested method
  2. closes over self, not caller locals
  3. strict arity, return leaves the method
  4. one block per call, many callables
  5. an allocation and a slower call

basics

~20 s

A Method object is better when the behaviour already exists as a named method, needs its object's state, or several callables are passed at once. A block is better for short inline logic that uses local variables, and yield is cheaper.

solid answer

~30 s

Passing `sheet.method(:format_row)` reuses a named method that is already tested and documented, keeps `self` and the object's instance variables available, and can be stored or passed alongside other callables, whereas a call takes only one block. It behaves like a method: arguments are checked strictly and `return` leaves the method. A block wins for short, one-off logic, because it sees the caller's local variables, reads at the call site and costs least when the callee simply yields. A `Method` object costs an allocation for each `method(:name)` call and a slower dispatch than a direct call or `yield`.

code

ruby · 23 lines
ruby
class ReportGenerator
  def initialize(formatter:)
    @formatter = formatter           # anything that responds to call
  end

  def render(rows) = rows.map { |row| @formatter.call(row) }.join("\n")
end

class PriceSheet
  def initialize(currency)
    @currency = currency
  end

  def format_row(row) = "#{row[:sku]}: #{row[:price]} #{@currency}"
end

rows = [{sku: "A1", price: 9.5}]
sheet = PriceSheet.new("EUR")

ReportGenerator.new(formatter: sheet.method(:format_row)).render(rows)
# => "A1: 9.5 EUR"
ReportGenerator.new(formatter: ->(row) { row[:sku] }).render(rows)
# => "A1"

go deeper

for a junior

Recall that you can pass an existing method as method(:name) wherever a callable is accepted, instead of writing a block.

for a middle

Compare what each carries: a Method object has its receiver and strict arguments, a block has the caller's locals, and only one block fits a call.

for a senior

Design APIs that accept any callable, avoid per-row method(:name) allocations in hot paths, and pick blocks where local context matters.

for a principal

Set conventions for extension points, such as callables versus blocks, so plug-in behaviour stays testable, discoverable in backtraces and cheap in hot paths.

## Two ways to hand behaviour to a report generator A report generator needs to turn each row into a line of text. The caller can supply that behaviour in two ways: - a **block**: `generator.render(rows) { |row| "#{row[:sku]}: #{row[:price]}" }`, or - a **`Method` object** for an existing method: `ReportGenerator.new(formatter: sheet.method(:format_row))`. Both are callables, but they carry different things and fit different designs. ## What each one closes over | Aspect | Block or lambda | `Method` object | |---|---|---| | `self` inside | the caller's `self` | the receiver it was taken from | | Caller's local variables | visible | not visible; a method body sees only its parameters | | Receiver's instance variables | only if the caller is that object | visible | | Argument checking | lenient for a block, strict for a lambda | strict, like any method call | | `return` inside | a block's leaves the method it was written in; a lambda's leaves the lambda | leaves the method only | | How many per call | one block | any number, as ordinary arguments | A `Method` object never sees the caller's locals because a `def` body starts a new scope; everything it needs must be a parameter or live on its receiver. ## When a Method object is the better choice 1. **The behaviour already exists as a method** with a name, tests and documentation, so writing `{ |row| sheet.format_row(row) }` would only add noise. 2. **The callable is stored** for later, for example in an instance variable of the generator or a table of formatters keyed by report type. 3. **Several callables are needed at once**: `header_formatter:`, `row_formatter:` and `footer_formatter:` can all be `Method` objects, whereas the call can take only one block. 4. **The formatter needs its object's state**, such as a currency or locale set in `initialize`. 5. **Introspection helps**: the generator can check `formatter.arity` or `formatter.owner` in an error message. ## When a block is the better choice - The logic is short and specific to one call site; inline code is easier to read than a jump to another method. - It needs **local variables** from the caller, such as a threshold computed a line above. - The callee uses **`yield`**: calling a block through `yield` is cheaper than creating a `Method` object and dispatching through `Method#call`. - The code runs in a tight loop where the extra object per `method(:name)` call is measurable. ## The same formatter four ways | Supplied as | Example | Sees caller locals | Carries object state | |---|---|---|---| | Block | `render(rows) { \|row\| row[:sku] }` | Yes | Only through the caller's `self` | | Lambda | `->(row) { row[:sku] }` | Yes | Only through the caller's `self` | | `Method` object | `sheet.method(:format_row)` | No | Yes, the receiver's | | Callable class | `SkuFormatter.new` with a `call` method | No | Yes, its own instance variables | The last two suit behaviour that already has a home and a name; the first two suit logic that belongs to one call site. ## Designing the generator's API Accept **any object that responds to `call`**. Then callers may pass a `Method`, a lambda or a small class with a `call` method, and the generator does not care which. If the generator also wants to accept a block, capture it and treat it as one more callable, so there is a single code path. ## Costs to keep in mind - Each `method(:format_row)` call allocates a new `Method`; take it once and reuse it rather than calling `method` per row. - `Method#call` goes through an extra layer compared with calling `format_row` directly. - A backtrace from inside the formatter shows `PriceSheet#format_row`, which helps when the method is shared and hides where the callable was handed over.

  • Why should the generator not call method(:format_row) once per row?
    Each `method(:name)` call looks the method up again and allocates a new `Method` object. Taking it once, storing it and calling it per row avoids repeated allocations; for the hottest loops, a direct call or `yield` is cheaper still.
  • A formatter needs a discount computed in the calling method. Which callable fits better?
    A block or lambda written in that method, because it can read the local `discount` directly. A `Method` object's body sees only its parameters and its receiver's state, so the discount would have to be passed as an argument or stored on the receiver first.

A Method object is like lending out a colleague who already knows the job and brings their own notes, while a block is a sticky note you write on the spot, which can mention anything on your desk.

saying these in an interview costs you the question

  • A Method object can read the local variables where it is called
  • A method call can take several blocks
  • A Method object is always faster than a block
  • return inside a Method object leaves the caller's method
  • Method objects accept extra arguments and ignore them