skip to content

Callable Objects

Ruby's blocks, procs, lambdas and Method objects: yielding, converting with &, and closing over local scope. Interviewers love proc vs lambda because it shows how a closure returns.

on this pageshow

explore

questions

24

In Ruby, what happens step by step when you write users.map(&:email), and what does the & do to the Symbol?

level: juniorimportance: must knowfreq 72%

answer

  1. & turns an argument into the block
  2. non-Proc: call to_proc
  3. Symbol#to_proc sends the method
  4. first yielded value is the receiver
  5. a lambda since Ruby 3.0

basics

~20 s

The & marks :email as the block for map. Because a Symbol is not a Proc, Ruby converts it with Symbol#to_proc, a lambda that calls email on each yielded user, so the result equals users.map { |user| user.email }.

solid answer

~40 s

A `&` before the last argument of a call means "use this object as the block". A `Proc` is used as it is; anything else is converted through `to_proc`. `Symbol#to_proc` returns a lambda that takes the first value it receives as the receiver and calls the method named by the symbol on it, passing any remaining values as arguments. So `users.map(&:email)` behaves like `users.map { |user| user.email }` and returns an array of addresses. Details interviewers probe: since Ruby 3.0 the proc is a lambda (`:email.to_proc.lambda?` is `true`, arity `-2`); the call is a public call, so a private `email` raises `NoMethodError`; and extra yielded values become arguments, so `users.each_with_index(&:email)` calls `email(index)` and raises `ArgumentError` for a zero-argument method. You cannot pass arguments of your own: `map(&:round(2))` is a `SyntaxError`.

code

ruby · 11 lines
ruby
User = Data.define(:name, :email)
users = [User.new(name: "Ada", email: "[email protected]"),
         User.new(name: "Lin", email: "[email protected]")]

users.map(&:email)              # => ["[email protected]", "[email protected]"]

to_email = :email.to_proc
to_email.lambda?                # => true (Ruby 3.0+)
to_email.call(users.last)       # => "[email protected]"

users.each_with_index(&:email)  # ArgumentError: email receives the index

go deeper

for a junior

Recall that users.map(&:email) is shorthand for a block calling email on each element, and that & turns the symbol into a block through to_proc.

for a middle

Explain the & conversion order (nil, Proc, to_proc), how Symbol#to_proc treats the first value as receiver and passes the rest as arguments, and the inject(&:+) case.

for a senior

Cover the edge cases that bite in production: public-only calls raising NoMethodError on private readers, extra yielded values breaking zero-argument methods, and the Ruby 3.0 lambda change.

for a principal

Set a readable convention for the shorthand in a shared code base: where &:name clarifies intent and where an explicit block or it communicates better to future maintainers.

## The & in a method call In Ruby, a **block** is normally written literally after a call. The **`&` prefix** on the last argument lets you supply the block as an object instead: ```ruby User = Data.define(:name, :email) users = [ User.new(name: "Ada", email: "[email protected]"), User.new(name: "Lin", email: "[email protected]") ] users.map(&:email) # => ["[email protected]", "[email protected]"] users.map { |user| user.email } # the same result ``` What Ruby does with `&obj` depends on the object: 1. **`nil`**: the call gets no block at all. 2. **A `Proc`**: that proc becomes the block unchanged. 3. **Anything else**: Ruby calls `obj.to_proc` and uses the returned `Proc`. If there is no `to_proc`, or it returns something that is not a `Proc`, the call raises `TypeError`. A `Symbol` falls into case 3, so the work is done by **`Symbol#to_proc`**. ## What Symbol#to_proc builds `:email.to_proc` returns a `Proc` that: - takes the **first** value it receives as the **receiver**; - calls the method named `email` on it; - passes any **remaining** values as arguments to that method. ```ruby to_email = :email.to_proc to_email.call(users.first) # => "[email protected]" to_email.lambda? # => true to_email.arity # => -2 ``` For `users.map(&:email)`, `map` yields each user, the proc calls `user.email`, and `map` collects the results. | Expression | Equivalent block | |---|---| | `users.map(&:email)` | `users.map { \|u\| u.email }` | | `names.each(&:strip!)` | `names.each { \|n\| n.strip! }` | | `[1, 2, 3].inject(&:+)` | `[1, 2, 3].inject { \|sum, n\| sum + n }` | The last row shows the "remaining values become arguments" rule: `inject` yields two values, so the proc calls `sum.+(n)`. ## Version note Since **Ruby 3.0**, `Symbol#to_proc` returns a **lambda**: `lambda?` is `true` and `arity` is `-2` (one required receiver plus any number of arguments). Earlier releases returned a regular proc. The change matters only when code inspects the proc, for example a library that branches on `lambda?` or `arity`. ## Edge cases interviewers ask about - **Visibility.** The method is called as a public call, so if `email` is `private`, `users.map(&:email)` raises `NoMethodError` even though `users.map { |u| u.send(:email) }` would work. - **Extra yielded values.** `each_with_index` yields the element and its index, so `users.each_with_index(&:email)` calls `user.email(0)` and raises `ArgumentError` for a method that takes no arguments. - **No extra arguments of your own.** `prices.map(&:round(2))` is a `SyntaxError`, because `&` takes one object and `:round(2)` is not an expression. Write a block: `prices.map { it.round(2) }`. - **Method missing.** If the receiver handles the name through `method_missing`, the proc still works, because it performs an ordinary method call. ## Three ways to write the same map | Form | Reads as | Notes | |---|---|---| | `users.map(&:email)` | call `email` on each | public call only, no extra arguments | | `users.map { it.email }` | same, Ruby 3.4+ | allows arguments and chains, e.g. `it.email.downcase` | | `users.map { \|user\| user.email }` | same, named | clearest once the block grows | All three return the same array for public, zero-argument readers. The differences only show up at the edges: visibility, arguments and what extra values the iterator yields. ## Common mistakes 1. **Chaining inside the shorthand.** `&:email` can name one method only; for `email.downcase` either chain two maps, `users.map(&:email).map(&:downcase)`, or write a block. 2. **Using it with iterators that yield several values.** `each_with_index`, `each_with_object` and `inject` pass extra values as arguments, which is useful for `inject(&:+)` and an `ArgumentError` for a zero-argument reader. 3. **Assuming a private reader works.** The shorthand is a public call, so a `private` `email` raises `NoMethodError`. ## When to use it `&:name` is idiomatic for **one method call with no arguments** on each element: `map(&:email)`, `select(&:active?)`, `sum(&:amount)`. As soon as you need arguments, a chain of calls or a condition, a literal block (or `it`, on Ruby 3.4+) is clearer than any trick with `&`. Readers of Ruby code expect the shorthand, so using it where it fits is a style point, not cleverness.

  • Why does users.map(&:email) raise NoMethodError when email is a private method?
    The proc from `Symbol#to_proc` performs a public method call on the receiver, as if written `user.email` from outside the object. Private methods cannot be called with an explicit receiver that way, so Ruby raises `NoMethodError`. A literal block using `send` would bypass visibility, but exposing the reader publicly is usually the right fix.
  • What does [1, 2, 3].inject(&:+) return, and why does it work?
    It returns 6. `inject` yields two values, the running total and the next element; the proc treats the first as the receiver and passes the second as the argument, so each step computes `total.+(element)`.

saying these in an interview costs you the question

  • &:email calls a method named to_proc on each user
  • Symbol#to_proc returns a regular proc, not a lambda, on Ruby 4.0
  • &:email can call private methods because it uses send
  • map(&:round(2)) passes 2 to round for each element
  • Passing &:email to a method that yields two values ignores the second value
open as a page

In Ruby, which outer local variables can a block read and reassign, and why is a variable first assigned inside it gone afterwards?

level: juniorimportance: must knowfreq 58%

basics

~20 s

A Ruby block can read and reassign every local already assigned above it in the enclosing scope. A name first assigned inside the block is local to that block run, so using it after the block raises NameError.

open as a page

In Ruby, how does a method run the block it was called with using yield, and what does block_given? protect against?

level: juniorimportance: must knowfreq 78%

basics

~20 s

yield runs the block attached to the current method call, passes its arguments to the block's parameters and evaluates to the block's last value. With no block, yield raises LocalJumpError; block_given? lets the method check first.

open as a page

In Ruby, what does an explicit &block parameter give a method, and how do you pass that block on to another method?

level: middleimportance: must knowfreq 62%

basics

~20 s

An &block parameter captures the call's block as a Proc object, or nil when no block was given. The method can call, store or test it, and forwards it to another method as a block by writing other(&block).

open as a page

In Ruby, how can a lambda returned from a method keep incrementing a local variable after that method has returned?

level: middleimportance: must knowfreq 62%

basics

~20 s

A Ruby lambda closes over the local variable itself, not a copy of its value. When the lambda is created, CRuby moves the method's locals into a heap environment, so the lambda can still read and reassign clicks after the method returns.

open as a page

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%

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.

open as a page

In Ruby, what are the differences between a proc and a lambda, and when would you choose each?

level: middleimportance: must knowfreq 80%

basics

~20 s

Both are Proc objects. A lambda (lambda, ->) checks arguments like a method and its return exits only the lambda; a proc (proc, Proc.new) nil-fills or drops positional arguments, and its return exits the enclosing method.

open as a page

In Ruby, why does puts [1, 2, 3].map do |n| n * 2 end print an Enumerator instead of the doubled numbers?

level: middleimportance: must knowfreq 55%

basics

~20 s

do...end binds more loosely than braces, so in a call without parentheses the block goes to puts, not map. map runs without a block and returns an Enumerator, and puts prints that object and ignores the block.

open as a page

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%

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.

open as a page

In Ruby, which of proc, Proc.new, lambda and the -> literal create lambdas, and how do you check one?

level: juniorimportance: should knowfreq 50%

basics

~10 s

lambda { } and ->(x) { } create lambdas; proc { } and Proc.new { } create regular procs. All four return a Proc, so check with Proc#lambda?, which is true only for lambdas.

open as a page

In Ruby, what does & do with a non-Proc argument such as &method(:normalize_email) or a Hash, and when does it raise TypeError?

level: middleimportance: should knowfreq 38%

basics

~20 s

& calls to_proc on any argument that is not already a Proc or nil and uses the result as the block. Method#to_proc and Hash#to_proc return lambdas; an object without to_proc, or whose to_proc returns a non-Proc, raises TypeError.

open as a page

In Ruby, how do you memoise a price lookup in a lambda that closes over a Hash, and why can cache[sku] ||= miss the cache?

level: middleimportance: should knowfreq 38%

basics

~20 s

Create the Hash as a local in a factory method and return a lambda that checks it before computing; every call shares that Hash. cache[sku] ||= price_for(sku) recomputes whenever the stored price is nil or false, so check key? instead.

open as a page

In Ruby, what do Method#arity and Method#parameters report for a method with optional, splat and keyword parameters?

level: middleimportance: should knowfreq 32%

basics

~20 s

Method#arity returns the required count when it is fixed, otherwise -n-1 where n counts required arguments; keywords count as one argument. Method#parameters lists each parameter as [kind, name], with kinds such as :req, :opt, :rest, :keyreq, :key, :keyrest and :block.

open as a page

In Ruby, what do the >> and << operators return for Proc and Method objects, and which callable runs first in each?

level: middleimportance: should knowfreq 30%

basics

~20 s

Both return a new Proc. f >> g calls f with the arguments, then g with f's result; f << g calls g first, then f. The operand may be a Proc, a Method or any object responding to call.

open as a page

In Ruby 4.0, how do a proc and a lambda handle missing, extra and single-array arguments when called?

level: middleimportance: should knowfreq 45%

basics

~10 s

A lambda checks arguments like a method, raising ArgumentError on any count mismatch. A regular proc fills missing positional parameters with nil, discards extra ones, and splats a single array across several parameters.

open as a page

In Ruby, what does a yield expression evaluate to, and how should a with_timing helper hand the block's result back to its caller?

level: middleimportance: should knowfreq 45%

basics

~20 s

yield evaluates to the block's last expression, or nil for an empty block. A with_timing wrapper should make yield its return value, for example by timing in an ensure clause, instead of ending with a logging call that returns nil.

open as a page

In Ruby, why can a lambda kept in a long-lived registry hold a large local array and its creating object in memory, and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A CRuby lambda keeps the whole environment it was created in: every local of the enclosing scopes plus self, not only the names its body uses. A large array local stays reachable while the lambda lives, so build long-lived lambdas in small methods.

open as a page

In Ruby, how do Proc#curry and Method#curry decide when to run, and why does currying a method with optional parameters need an explicit arity?

level: seniorimportance: should knowfreq 24%

basics

~20 s

A curried proc collects arguments until it has as many as its arity, then calls the original. The default arity is the required count, so for def cell(value, width = 10) method(:cell).curry runs after one argument; pass curry(2) to wait for width.

open as a page

In Ruby, what do Module#instance_method, UnboundMethod#bind and bind_call do, and why use them to run an implementation a subclass overrides?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Module#instance_method returns an UnboundMethod, a definition without a receiver. bind(obj) attaches it to a compatible object and returns a Method; bind_call(obj, *args) binds and calls in one step. That runs one exact definition regardless of overrides.

open as a page

In Ruby, why does a form-validation rule built in a method as proc { |v| return false if v.nil?; true } raise LocalJumpError later?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A regular proc's return targets the method that created it. Once that method has returned the proc is orphaned, so return raises LocalJumpError (unexpected return). Build the rule as a lambda, or use next false, to leave only the callable.

open as a page

In Ruby 3.4 and later, how does the implicit it block parameter differ from the numbered parameter _1, and where is each rejected?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Both name a block's first argument without declaring it. it (Ruby 3.4) nests freely and is only a soft keyword; _1 (Ruby 2.7) comes with _2 to _9, cannot be used in nested numbered blocks and is reserved.

open as a page

In Ruby, what does declaring a block-local variable with |row; total| do, and when would you reach for one?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

Names listed after a semicolon in a block's parameter list are block-local variables: fresh locals that start as nil on each run and never touch an outer variable with the same name, even when the block assigns to them.

open as a page

In CRuby 4.0, when does an &block parameter actually allocate a Proc object, and which uses of it avoid that allocation?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

CRuby creates the Proc lazily: forwarding with other(&block), calling block.call and testing if block run without allocating. Using the parameter as a value, such as storing, returning or passing it positionally, builds the Proc once per call.

open as a page

In Ruby 4.0, what do Kernel#binding and Binding#local_variable_get/set expose, and why is a local created by local_variable_set invisible to the method?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Kernel#binding returns a Binding wrapping the current scope's locals and self. local_variable_get and local_variable_set read and write existing locals by name; setting a new name adds it only to the Binding, because the method's code was compiled without it.

open as a page