skip to content

In Ruby, what does calling method(:format_row) on an object return, and how do you invoke the result later?

level: juniorimportance: should knowfreq 40%

answer

  1. an object, not a result
  2. bound to its receiver
  3. call, .() and []
  4. private methods too; NameError if missing
  5. name, receiver, owner

basics

~10 s

Kernel#method returns a Method object: format_row bound to that receiver, without running it. Invoke it later with call, .() or [], passing arguments as usual; an unknown name raises NameError.

solid answer

~30 s

`report.method(:format_row)` looks the method up the way a call on `report` would and wraps the definition together with its receiver in a `Method` object; nothing runs yet. Later, `m.call(row)`, `m.(row)` and `m[row]` all run it with `self` set to `report`, so the body can use `report`'s instance variables. `method` finds private methods as well, while `public_method` returns public ones only; an unknown name raises `NameError`. The object also answers questions about itself: `name` is `:format_row`, `receiver` is `report`, and `owner` is the class or module that defines the method.

code

ruby · 19 lines
ruby
class PriceSheet
  def initialize(currency)
    @currency = currency
  end

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

sheet = PriceSheet.new("EUR")
m = sheet.method(:format_row)   # nothing runs yet

m.call({sku: "A1", price: 9.5})  # => "A1: 9.5 EUR"
m.({sku: "B2", price: 3})        # => "B2: 3 EUR"
m[{sku: "C3", price: 1}]         # => "C3: 1 EUR"

m.name       # => :format_row
m.receiver   # => the sheet
m.owner      # => PriceSheet
sheet.method(:nope)              # NameError

go deeper

for a junior

Remember that method(:name) returns an object you call later with call, .() or [], and that it runs on the object you took it from.

for a middle

Explain what the Method holds, the receiver and the definition, and the difference between method and public_method, name, receiver and owner.

for a senior

Use Method objects to hand existing, tested behaviour to APIs that accept any callable, and know that lookup errors surface immediately as NameError.

for a principal

Decide when public APIs should accept any object responding to call, which lets callers pass Method objects, lambdas or small classes interchangeably.

## Getting a Method object In Ruby a method is not normally a value you can hold: `report.format_row(row)` simply calls it. **`Kernel#method`** turns a method into an object you can store and pass around. `report.method(:format_row)` (a Symbol or a String works) finds `format_row` the same way a call on `report` would, through the object's singleton class, its class, included modules and superclasses, and returns an instance of **`Method`**. That object holds two things: - the **definition** it found, and - the **receiver**, `report`, the object the method will run on. Nothing has been executed. The core documentation describes a `Method` as acting like a closure over its object: instance variables and `self` stay available whenever it runs. ## Calling it | Form | Meaning | |---|---| | `m.call(row)` | the explicit call | | `m.(row)` | syntax sugar for `call` | | `m[row]` | alias of `call` | | `m === row` | also calls it, which lets a `Method` appear in a `when` clause | The `call`, `.()` and `[]` forms pass positional arguments, keyword arguments and a block through to the method exactly as a direct call would. The number of arguments is checked like any method call, so a wrong count raises `ArgumentError`. ## Lookup rules 1. **Private methods are included.** `obj.method(:helper)` returns a `Method` even if `helper` is private, and calling it works, because you went through the object deliberately. 2. **`public_method`** is the strict variant: it raises `NameError` for private and protected methods. 3. **Unknown names raise `NameError`** at lookup time, not later when the object is called, which surfaces a typo where it was made. 4. The receiver can be any object, including a class: `Integer.method(:sqrt)` wraps a class method. ## Asking the object about itself - `m.name` returns the Symbol, `:format_row`. - `m.receiver` returns the bound object, `report`. - `m.owner` returns the class or module where the definition lives, which may be a module mixed into `report`'s class. - `m.unbind` returns an `UnboundMethod` that has forgotten the receiver, which can later be bound to another compatible object. - `m.arity` and `m.parameters` describe the parameters. ## Lookup happens once The `Method` object keeps the definition it found when `method` was called: - A typo fails immediately with `NameError`, at the line that asked for the method, rather than later inside the code that calls it. - If the class later **redefines** `format_row`, a `Method` taken earlier still runs the old body, while a fresh `sheet.format_row(row)` runs the new one. Take the `Method` again after a reload if you need the new code. - Each call to `method(:format_row)` performs the lookup and allocates a new `Method`, so take it once and keep it when it will be called many times. ## Why a report generator cares Suppose a `ReportGenerator` accepts anything that responds to `call` as its row formatter. An existing, tested `PriceSheet#format_row` can be handed over as `sheet.method(:format_row)` without writing a wrapper block, and the generator stays unaware of whether it got a `Method`, a lambda or a custom object with a `call` method. Walking through it: 1. `sheet.method(:format_row)` finds `PriceSheet#format_row` and binds it to `sheet`. 2. The generator stores the object and, for each row, calls `@formatter.call(row)`. 3. Each call runs `format_row` with `self` equal to `sheet`, so `@currency` is available. 4. The generator never needed to know the method's name or its class. ## Common slips - Writing `method(:format_row)` and expecting the formatted string: it returns the `Method`, not a result. - Expecting `m.unbind.call(row)` to work: an `UnboundMethod` has no `call` until it is bound again. - Assuming `method` refuses private methods; that is `public_method`.

  • What is the difference between Method#owner and Method#receiver?
    `receiver` is the object the method is bound to and will run on. `owner` is the class or module whose method table holds the definition. For a method mixed in from a module, `owner` is that module even though `receiver` is an instance of some class that includes it.
  • Does obj.method(:helper) work when helper is private?
    Yes. `Kernel#method` returns private and protected methods as well, and calling the resulting `Method` runs them. Use `public_method` when you want lookup to fail for anything that is not public, for example when the name comes from outside.

saying these in an interview costs you the question

  • method(:format_row) calls the method and returns its result
  • A Method object forgets its receiver until call is given one
  • method raises NameError for private methods
  • An UnboundMethod from unbind can be called directly
  • A Method object takes any number of arguments, like a block