In Ruby, what does an explicit &block parameter give a method, and how do you pass that block on to another method?
answer
- last parameter, prefixed with &
- a Proc, or nil without a block
- call it, store it, test it
- forward with &block, not block
- yield still works alongside it
basics
~20 sAn &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).
solid answer
~40 sDeclaring `&block` as the last parameter turns the caller's block into a `Proc` bound to `block`; when no block is passed, `block` is `nil`. That object can be called (`block.call(user)`, `block.(user)` or `block[user]`), stored for later, such as a callback kept in an instance variable, tested with `if block`, or handed on. To hand it on **as a block**, write `other_method(&block)`: the `&` passes the existing `Proc` through unchanged. Writing `other_method(block)` instead passes the `Proc` as an ordinary positional argument, so `other_method` receives no block and usually fails with `ArgumentError` or runs blockless. A captured block is a regular proc unless the caller passed a lambda with `&`, and `yield` still works in a method that declares `&block`.
code
ruby · 15 linesdef export_emails(users, &formatter)
return users.map(&:email) unless formatter
users.map(&formatter) # forwards the caller's block to map
end
User = Data.define(:name, :email)
users = [User.new(name: "Ada", email: "[email protected]")]
export_emails(users) # => ["[email protected]"]
export_emails(users) { |u| u.email.upcase } # => ["[email protected]"]
def broken(users, &formatter)
users.map(formatter) # ArgumentError: map takes no positional arguments
endgo deeper
Recall that &block names the caller's block as a Proc, that it is nil without a block, and that forwarding needs & at the call site.
Explain the difference between other(&block), other(block) and other { block }, why a nil block turns map into a blockless call, and when capturing beats yield.
Design callback APIs that store blocks safely, default a missing block sensibly, and keep lambda or proc semantics predictable for callers who pass either.
Decide when an API should take blocks, explicit callable arguments or both, considering that a method gets one block but can accept many callables.
## From implicit block to named object Every Ruby method call can carry a block. Without any declaration, the method can only run it with `yield`. Adding an **`&block` parameter** (any name works; `block` is conventional) gives the block a name as an object: ```ruby def export_emails(users, &formatter) formatter ||= :email.to_proc users.map(&formatter) end export_emails(users) # => ["[email protected]", ...] export_emails(users) { |u| "#{u.name} <#{u.email}>" } ``` Rules for the parameter: 1. It must be the **last** parameter, after positional, splat and keyword parameters. 2. Its value is a **`Proc`** when a block was given and **`nil`** when not. 3. A literal block arrives as a **regular proc** (`lambda?` is `false`); if the caller wrote `&some_lambda`, the parameter holds that same lambda object. 4. `yield` and `block_given?` keep working; `&block` just adds a name. ## What the name lets you do With the block as an object, the method can: - **call it** with `block.call(args)`, `block.(args)` or `block[args]`; - **store it** for later, which `yield` cannot do because `yield` only works while the method is running; - **test it** with `if block` or `block.nil?` as an alternative to `block_given?`; - **pass it on** to another method as that method's block. Storing is the main reason to capture. A callback registry is the classic case: ```ruby class EmailExport def on_skip(&handler) @on_skip = handler self end def run(users) valid, skipped = users.partition { |u| u.email.to_s.include?("@") } skipped.each { |u| @on_skip&.call(u) } valid.map(&:email) end end ``` ## Passing the block along The critical detail is the `&` at the **call site**: | Call | What the callee receives | |---|---| | `users.map(&formatter)` | `formatter` as its block | | `users.map(formatter)` | `formatter` as a positional argument, no block | | `users.map { formatter }` | a new block that returns the `Proc` itself | | `users.map(&nil)` | no block at all | - `Array#map` takes no positional arguments, so `users.map(formatter)` raises `ArgumentError` (wrong number of arguments). - `users.map { formatter }` runs fine and returns an array of `Proc` objects, a quiet bug. - `&nil` passing no block is why `users.map(&formatter)` with a `nil` formatter returns an `Enumerator`: `map` ran blockless. The `||=` default in the first example prevents that. ## Explicit parameter or yield? | Need | Choose | |---|---| | Run the block now, maybe several times | `yield` | | Keep the block after the method returns | `&block` | | Forward the block to another method | `&block` (or anonymous forwarding) | | Introspect it, for example `lambda?` | `&block` | Since Ruby 3.1 a method that only passes its block along may leave the parameter anonymous, `def run(&)` with `other(&)`; the details of that form belong with method parameters. ## Tracing a forwarded block Follow `export_emails(users) { |u| u.email.upcase }` through the first example: 1. The caller's literal block arrives as `formatter`, a regular `Proc`. 2. `formatter ||= ...` leaves it alone, because it is not `nil`. 3. `users.map(&formatter)` passes the same `Proc` to `map` as its block; no new wrapper is created. 4. `map` yields each user to that proc and collects the upcased addresses. 5. With no block, step 1 gives `nil`, step 2 substitutes `:email.to_proc`, and `map` returns plain addresses instead of an `Enumerator`. ## Proc or lambda after capture The captured object keeps the semantics it was created with. A literal block becomes a regular proc, so it tolerates missing or extra arguments. If a caller passes `&->(u) { u.email }`, the parameter holds that lambda, and calling it with the wrong number of arguments raises `ArgumentError`. Library code that calls stored blocks should pass exactly the arguments it documents so both kinds behave the same. ## Common mistakes - Forgetting the `&` when forwarding, which turns the block into a positional argument. - Calling `block.call` without checking for `nil` when the block is optional, which raises `NoMethodError` on `nil`. - Capturing `&block` only to `yield` later; if you never use the object, `yield` alone is simpler.
- If a caller writes export_emails(users, &->(u) { u.email }), is formatter a lambda inside the method?Yes. When the argument after `&` is already a `Proc`, Ruby passes that exact object through, so the parameter holds the same lambda and `formatter.lambda?` is `true`. Only a literal block creates a new regular proc.
- Why can a method store &block for later but not do the same with yield?`yield` refers to the block of the method call that is currently running; once the method returns there is nothing left to yield to. Capturing the block as a `Proc` produces an ordinary object that can be kept in a variable and called after the method has returned.
saying these in an interview costs you the question
- Passing block without & forwards it as the callee's block
- An &block parameter is an empty Proc when no block is given
- Declaring &block disables yield in that method
- A captured literal block is always a lambda
- A method can declare two & parameters to accept two blocks