Blocks, Procs and Lambdas
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18). Features newer than Ruby 3.0 name the version that
introduced them, and every example was run on Ruby 4.0 with warnings enabled.
A block is the chunk of code a method call carries with it; a proc is that code turned into an object; a lambda is a
proc with method-like rules; and a closure is any of them together with the variables it can still see. Almost
everything idiomatic in Ruby, from each and map to File.open, Struct.new and every DSL, rests on this one
mechanism. This page covers the whole of it: the syntax of blocks and their parameters, the numbered parameters and the
it parameter, the exact differences between procs and lambdas (arity, return, break and next), closures and
Binding, Method and UnboundMethod objects, to_proc and &, currying and composition, and callable objects as a
design tool.
It builds on Methods and Parameters (the &block parameter
and the other parameter kinds) and
Calling Methods and Argument Forwarding
(passing blocks with &, anonymous forwarding). Later pages rely on it:
Enumerable,
Enumerators and Lazy Evaluation,
Dynamic Dispatch and Method Definition and
Hooks, eval and DSLs. How scope rules decide which variables a
block can see is in Variables, Constants and Scope,
and break, next and redo as statements are in
Control Expressions. For other languages' approaches, see
Go closures,
Rust closures,
Swift closures,
Kotlin lambdas,
Groovy closures,
Java lambdas,
C# delegates and lambdas and
PHP closures.
Examples that use the shelf scenario (the small library catalogue gem used throughout this section) call
require "shelf" (see the shelf gem source); the others are self-contained. Output is the real output of the code as shown, with # => marking the
value of the line above (using the Ruby 3.4+ inspect format) and # prints: marking what puts writes.
The three objects at a glance
| Block | Proc | Lambda | |
|---|---|---|---|
What it is |
Syntax attached to one method call; not an object |
An instance of |
An instance of |
Created by |
|
|
|
Called by |
|
|
the same five forms |
Wrong number of arguments |
Missing ones are |
Missing ones are |
|
|
Returns from the method that wrote the block |
Returns from the method that wrote the proc ( |
Returns from the lambda only |
|
Ends the method the block was given to |
|
Returns from the lambda only |
|
Ends this call of the block, giving a value |
Ends this call of the proc, giving a value |
Ends this call of the lambda, giving a value |
The rest of the page justifies every cell of this table.
Blocks
Syntax: braces versus do and end
A block is written after the arguments of a method call, either between braces or between do and end. Both forms
mean the same thing except for precedence: braces bind to the nearest call (the one just to their left), while do ... end
binds to the whole command. The difference only shows when a call has no parentheses, because then there are two calls
the block could belong to. The precedence table in the language reference lists { ... } blocks above do ... end
blocks, and the calling-methods reference states the same rule.
# frozen_string_literal: true
titles = %w[dune emma ulysses]
# Braces bind to the nearest call, do...end to the whole command.
braces = titles.map { |title| "#{title}!" }
p braces
# => ["dune!", "emma!", "ulysses!"]
p(titles.map { |title| title.length * 2 }.sum)
# => 30
# rubocop:disable Lint/AmbiguousBlockAssociation
# With do...end the block goes to p, not to map, so map returns an Enumerator.
p titles.map do |title|
"#{title}?"
end
# => #<Enumerator: ["dune", "emma", "ulysses"]:map>
# rubocop:enable Lint/AmbiguousBlockAssociation
# Assigning first keeps the block on map.
questions = titles.map do |title|
"#{title}?"
end
p questions
# => ["dune?", "emma?", "ulysses?"]
The convention that follows from this (and the default of RuboCop’s Style/BlockDelimiters) is braces for a one-line
block whose value is used, do ... end for a multi-line block or one that is called for its side effects. It is a
convention for readability, not a rule of the language; the real constraint is that a chained call after the block
(map { ... }.sum) needs the braces, or a parenthesised do ... end.
A method call has at most one block. The block is always the last thing in the call, and it is not an argument in the
ordinary sense: it is not part of the positional or keyword arguments, it is not counted by arity, and it is passed on
only explicitly (with &) or implicitly (by super). A block can be attached to any method call, including ones that
never use it; Ruby does not complain, which is why puts [1, 2].map do |n| ... end silently hands the block to puts.
yield, block_given? and the value of a block
Inside a method, yield runs the block that the method was called with, passing it the arguments given to yield and
returning the value of the block’s last expression. block_given? reports whether there is a block. Calling yield when
there is none raises LocalJumpError ("no block given (yield)").
# frozen_string_literal: true
# A method runs its block with yield and reports whether it was given one.
def audited(action)
return "no block for #{action}" unless block_given?
puts "start #{action}"
result = yield
puts "finish #{action}"
result
ensure
puts "audit closed"
end
p audited("lend") { 42 }
# prints: start lend
# prints: finish lend
# prints: audit closed
# => 42
p audited("lend")
# prints: audit closed
# => "no block for lend"
# yield passes arguments to the block and returns the block's value.
def each_numbered(items)
return enum_for(__method__, items) unless block_given?
items.each_with_index { |item, index| yield item, index + 1 }
end
each_numbered(%w[Dune Emma]) { |title, number| puts "#{number}. #{title}" }
# prints: 1. Dune
# prints: 2. Emma
p each_numbered(%w[Dune Emma]).to_a
# => [["Dune", 1], ["Emma", 2]]
def without_block = yield
begin
without_block
rescue LocalJumpError => e
puts "#{e.class}: #{e.message}"
end
# prints: LocalJumpError: no block given (yield)
Source: Kernel#block_given?, Object#enum_for, LocalJumpError
Points worth knowing:
-
The
return enum_for(__method__, ...) unless block_given?line is the standard way to let a custom iterator work both with a block and as anEnumerator(each_numbered(...).to_a,.next,.with_index, lazy chains). The shelfCatalog#eachuses the same idiom (see The shelf Catalog in practice).__method__names the current method, which survives renaming. -
The
ensureclause of a method runs when the block exits the method early (bybreak,return,throwor an exception), so cleanup around ayieldis reliable. This is the shape ofFile.openwith a block,Mutex#synchronizeandDir.chdirwith a block. -
Whatever the block returns is the value of the
yieldexpression, so a method can let its caller customise a decision:items.sum { |item| item.weight },array.sort_by { ... },hash.fetch(key) { default }. -
A block gets the arguments of
yieldwith block parameter semantics (below):yield a, bandyield [a, b]look alike to a|x, y|block but differ for|x|. -
yieldis also the cheapest way to call a block: it needs noProcobject. The language reference says to prefer a bareyieldwhen you only call the block and do not store or forward it.
Source: Methods: Block Argument
The &block parameter and anonymous forwarding
To store the block, return it or pass it on, give the method a parameter prefixed with &. Ruby converts the block into a
Proc and binds it to that name; the parameter is nil when no block was given. Since Ruby 3.1 the block parameter can
be anonymous (a bare &), which can then only be forwarded: def each(&) = @items.each(&).
At the call site, &expression does the reverse: it converts the value to a proc (by calling to_proc on it, unless it
is already a Proc) and passes it as the block. &nil passes no block.
# frozen_string_literal: true
# &block turns the block into a Proc; a bare & forwards it without a name (3.1).
def explicit(&block) = block
def forward(&) = explicit(&)
block = explicit { |title, copies = 1| "#{title} x#{copies}" }
p block.class, block.lambda?, block.arity
# => Proc
# => false
# => 1
p block.call("Dune")
# => "Dune x1"
p forward { 1 }.class
# => Proc
p explicit
# => nil
# Passing an existing Proc keeps its lambda-ness.
strict = ->(title) { "#{title}!" }
p explicit(&strict).lambda?
# => true
p(%w[a b].map(&strict))
# => ["a!", "b!"]
Rules for the conversion:
-
A
Procpassed with&stays the same object, keeps its lambda-ness and is not wrapped. The sameProcobject comes out of&blockthat went in, which is why lambdas passed as blocks keep their strict arity. -
A
Symbol,MethodorHashbecomes a lambda-like proc through its ownto_proc(seeto_procand the ampersand). -
Anything else raises
TypeError("no implicit conversion of Integer into Proc"; the message names the class). -
The anonymous
&can be forwarded from the method body, from a nested block or from a nested proc (all three work in Ruby 4.0), but only inside the method that declared it; name the parameter when more than one level needs it. -
The
...forwarding parameter includes the block, sodef wrapper(...) = target(...)forwards it too.
Block parameters
A block’s parameter list sits between vertical bars. It accepts every kind of parameter a method does (required,
optional, splat, keyword, keyword splat and &block), and also destructuring parentheses, but follows the lenient
proc rules for arguments:
-
Missing arguments are
nil; extra arguments are dropped. -
If the block has more than one parameter (or a trailing comma, or a splat together with other parameters) and receives exactly one argument that is an
Array, the array is spread over the parameters. This is whyhash.each { |key, value| }works althoughHash#eachyields one[key, value]pair, and why|a|receives the whole pair. -
Parentheses destructure a nested array:
|a, (b, c)|. They nest and combine with splats:|a, (b, *c), d|. -
A trailing comma (
|a, |) takes the first element and ignores the rest, the same as|a, *|. -
Keyword parameters are filled from a trailing hash that
yieldpasses as keywords (yield(key: 1)to|key:|). Since Ruby 3.2 a proc that takes exactly one positional parameter plus keywords no longer auto-splats an array argument.
# frozen_string_literal: true
# Procs and blocks are lenient: missing arguments are nil, extras are dropped.
pair = proc { |first, second| [first, second] }
p pair.call(1)
# => [1, nil]
p pair.call(1, 2, 3)
# => [1, 2]
p pair.call([1, 2])
# => [1, 2]
# Destructuring, splats, defaults, keywords and a block-local variable.
rows = [["Dune", %w[Frank Herbert], 1965]]
rows.each do |title, (first, last), year = 0, *rest, edition: 1|
p [title, first, last, year, rest, edition]
end
# => ["Dune", "Frank", "Herbert", 1965, [], 1]
[[1, [2, 3]], [4, [5, 6]]].each { |a, (b, c)| puts a + b + c }
# prints: 6
# prints: 15
{ "Dune" => 1965 }.each { |title, year| puts "#{title} (#{year})" }
# prints: Dune (1965)
p({ "Dune" => 1965 }.map { |entry| entry })
# => [["Dune", 1965]]
total = 100
[1, 2].each { |n; total| total = n } # rubocop:disable Lint/UselessAssignment
p total
# => 100
[1, 2].each { |n| total = n }
p total
# => 2
The auto-splat deserves care, because it is the main way a block differs from a lambda (see below) and from a method.
[[1, 2]].each { |a, b| } gives a = 1, b = 2; [[1, 2]].each { |a| } gives a = [1, 2];
[[1, 2]].each { |a, | } gives a = 1. With each_with_index or each_with_object, the pair is (element, index), so
destructuring the element needs parentheses: |(a, b), index|.
Block-local variables and scope
A block is a closure scope, not a scope gate: it can read and assign the variables of the code around it, while def,
class and module start a fresh scope that cannot. Variables first assigned inside the block are local to that call of
the block and invisible after it (NameError: "undefined local variable or method 'y' for main"). A parameter with the
same name as an outer variable shadows it for the block and leaves the outer variable untouched. To make a variable
block-local without making it a parameter, list it after a semicolon: |n; total|. The example above shows both the
shadowing form and the leaking form.
Since Ruby 2.6 there is no warning for shadowing an outer variable, so only a linter can tell you
(RuboCop’s Lint/ShadowingOuterLocalVariable exists but is disabled by default). The block-local list is rarely needed; if a block needs a private
temporary and the outer scope has the same name, renaming the temporary is almost always clearer.
Numbered parameters and the it parameter
For one-line blocks, Ruby lets you skip the parameter list:
-
_1,_2…_9(numbered parameters, Ruby 2.7) name the first, second, … parameter. The highest number used decides the number of parameters, so a block that uses_2takes two. -
it(Ruby 3.4) names the single parameter of a block that has none.
They follow these rules:
-
A block that uses
_1alone behaves like|x|(it receives the whole element, so[[1, 2]].map { _1 }is[[1, 2]]). A block that uses_1and_2behaves like|a, b|and destructures. -
itbehaves like_1used alone: one parameter, no auto-splat. In a lambda it makes the lambda take exactly one argument. -
You cannot combine an ordinary parameter list with
_1orit, you cannot mix_1withit, you cannot assign to_1, and you cannot use_1in a block nested inside a block that already uses it. Each is aSyntaxError: the messages are quoted in the table below. -
itis a soft keyword, not a reserved word. A bareitin a block without parameters is the implicit parameter, unless a local variable nameditis visible (then it is that variable:it = 5; [1, 2].map { it }is[5, 5]).itwith arguments or a block, or writtenit()orself.it, stays an ordinary method call, so RSpec’sit "does x" do ... endis unaffected. Nested blocks that each useitrefer to their own parameter. -
Proc#parametersreportsfor a proc usingitandfor a lambda (Ruby 4.0 prints[:opt]; Ruby 3.4 printed[:opt, nil]), andfor numbered parameters in a lambda. Theirarityis the number of parameters. -
Use them only where the meaning is obvious; the documentation advises limiting yourself to
_1and_2in one-line blocks, and RuboCop’sStyle/NumberedParametersLimitcaps it at one.
# frozen_string_literal: true
titles = %w[Dune Emma]
p(titles.map { _1.center(8, "*") }) # rubocop:disable Style/ItBlockParameter
# => ["**Dune**", "**Emma**"]
p(titles.map { it.center(8, "*") })
# => ["**Dune**", "**Emma**"]
# _1 alone behaves like |x|; _1 with _2 destructures like |a, b|.
p([[1, 2]].map { _1 })
# => [[1, 2]]
p([[1, 2]].map { it })
# => [[1, 2]]
p([[1, 2]].map { [_1, _2] }) # rubocop:disable Style/NumberedParametersLimit
# => [[1, 2]]
# The block's parameters as Ruby reports them.
p proc { it }.parameters
# => [[:opt]]
p(-> { it }.parameters)
# => [[:req]]
p(-> { [_1, _2] }.parameters) # rubocop:disable Style/NumberedParametersLimit
# => [[:req, :_1], [:req, :_2]]
p proc { [_1, _2] }.arity # rubocop:disable Style/NumberedParametersLimit
# => 2
# Both reach the block through Binding (4.0).
[10].each { p [it, binding.implicit_parameters, binding.implicit_parameter_defined?(:_1)] }
# => [10, [:it], false]
The syntax errors, copied from running each snippet (Prism, the default parser since Ruby 3.4):
| Code | Message |
|---|---|
|
numbered parameters are not allowed when an ordinary parameter is defined |
|
'it' is not allowed when an ordinary parameter is defined |
|
'it' is not allowed when a numbered parameter is already used |
|
numbered parameter is already used in outer block |
|
_1 is reserved for numbered parameters |
Ruby 4.0 changed how these hidden parameters can be read. Binding#local_variables no longer lists numbered parameters
(and local_variable_get, local_variable_set and local_variable_defined? refuse them), and three new methods,
Binding#implicit_parameters, implicit_parameter_get and implicit_parameter_defined?, report them instead. The last
line of the example uses them. Debugging tools and binding.irb are the main users.
Blocks and break, next, redo
Inside a block, next ends the current call of the block and gives its argument (default nil) as the block’s value:
[1, 2, 3].map { |n| next 0 if n == 2; n } is [1, 0, 3]. break ends the method the block was given to and gives its
argument as that method’s value ([1, 2, 3].each { |n| break n * 10 if n == 2 } is 20). return leaves the method in
which the block was written. redo restarts the current call of the block with the same arguments. For
escaping several levels of nested blocks at once, use catch and throw, covered with the
exception model. The next section shows the same
three keywords in procs and lambdas, where the answers differ.
Procs and lambdas
Creating them
A Proc is an object holding a block of code and the variables it was created with. There are four spellings:
-
Proc.new { |x| ... }andproc { |x| ... }build a proc with proc semantics (lambda?isfalse).procis the shorthand RuboCop prefers (Style/Proc). -
lambda { |x| ... }and the stabby literal->(x) { ... }build a proc with lambda semantics (lambda?istrue). RuboCop prefers the literal for one-line lambdas andlambdawithdo ... endfor multi-line ones (Style/Lambda). The stabby parameter list may omit the parentheses (-> x { x }), may be omitted entirely for numbered oritparameters (-> { it * 2 }), and can take ado ... endbody (->(x) do ... end).
A callable is invoked with any of five equivalent spellings: call, .(), [], yield and ===. The last is what
makes lambdas usable in case/when and in pattern matching (see
Callable objects as interfaces).
# frozen_string_literal: true
# rubocop:disable Style/Proc, Style/Lambda, Style/LambdaCall, Style/CaseEquality, Style/SymbolProc
# This file lists every spelling on purpose, including the ones RuboCop rewrites.
# Four ways to build a callable object from a block of code.
from_new = Proc.new { |title| title.upcase }
from_proc = proc { |title| title.upcase }
from_lambda = lambda { |title| title.upcase }
stabby = ->(title) { title.upcase }
[from_new, from_proc, from_lambda, stabby].each { |callable| p callable.lambda? }
# => false
# => false
# => true
# => true
# Five ways to call one.
p stabby.call("dune")
# => "DUNE"
p stabby.("dune")
# => "DUNE"
p stabby["dune"]
# => "DUNE"
p stabby.yield("dune")
# => "DUNE"
p(stabby === "dune")
# => "DUNE"
# Proc.new and proc need a block; lambda needs a literal one.
[-> { Proc.new }, -> { proc }, -> { lambda }, -> { lambda(&from_proc) }].each do |attempt|
attempt.call
rescue ArgumentError => e
puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: tried to create Proc object without a block
# prints: ArgumentError: tried to create Proc object without a block
# prints: ArgumentError: tried to create Proc object without a block
# prints: ArgumentError: the lambda method requires a literal block
# rubocop:enable Style/Proc, Style/Lambda, Style/LambdaCall, Style/CaseEquality, Style/SymbolProc
Source: Proc, Kernel#lambda, Kernel#proc
History that explains old code:
-
Before Ruby 3.0,
Proc.newandprocwithout a block quietly used the block of the surrounding method (2.7 warned). Since 3.0 both raiseArgumentError("tried to create Proc object without a block"). Legacy code relying on it must pass&blockexplicitly. -
lambda(&some_proc)used to return the proc unchanged, with a warning from 3.0. Since Ruby 3.3 it raisesArgumentError("the lambda method requires a literal block").lambda(&some_lambda)is accepted and keeps the lambda. (The language reference page forProcstill shows the old behaviour in its examples.) -
procandProc.newgiven a lambda via&return that lambda, lambda-ness intact. A proc cannot be turned into a lambda; wrap it instead (->(*args) { some_proc.call(*args) }). -
define_methodalways creates a method with lambda-like argument checking, even from a non-lambda proc, andMethod#to_procalways gives a lambda. See instance_exec and define_method.
Procs versus lambdas
lambda? tells the two apart, and the language reference calls the differences "tricks": a proc has them and a lambda does
not, so a lambda behaves like an anonymous method. There are exactly two differences, plus a few consequences of them.
Arguments. A lambda checks its arguments like a method: too few or too many raise ArgumentError ("wrong number of
arguments (given 1, expected 2)"), and there is no auto-splat, so strict.call([1, 2]) passes one argument. A proc fills
missing ones with nil, drops extras and auto-splats a single array. This also decides what happens when a lambda is
passed as a block: [[1, 2]].each(&->(a, b) { }) raises ArgumentError, because each yields one array. Since Ruby 3.0,
{ a: 1 }.each(&->(key, value) { }) raises too, since Hash#each yields one [key, value] pair; Hash#map happens to accept a two-parameter lambda,
which makes the difference easy to trip over; a lambda taking one parameter (->(pair) { }) always works.
Control flow. return and break inside a lambda leave the lambda and give it its value. Inside a proc, return
leaves the method that created the proc and break leaves the method that the block was given to, which is why
procs can be orphaned (next section). next is the same everywhere: it ends the current call and gives its value.
# frozen_string_literal: true
# rubocop:disable Style/Lambda
# Arity: a proc forgives, a lambda insists.
sloppy = proc { |first, second| [first, second] }
strict = lambda { |first, second| [first, second] }
p sloppy.call(1)
# => [1, nil]
p sloppy.call([1, 2])
# => [1, 2]
begin
strict.call(1)
rescue ArgumentError => e
puts "ArgumentError: #{e.message}"
end
begin
strict.call([1, 2])
rescue ArgumentError => e
puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: wrong number of arguments (given 1, expected 2)
# prints: ArgumentError: wrong number of arguments (given 1, expected 2)
p sloppy.arity, strict.arity
# => 2
# => 2
p proc { |first, second = 1| }.arity, lambda { |first, second = 1| }.arity
# => 1
# => -2
p proc { |*all| }.arity, lambda { |*all| }.arity
# => -1
# => -1
p sloppy.parameters, strict.parameters
# => [[:opt, :first], [:opt, :second]]
# => [[:req, :first], [:req, :second]]
p sloppy.parameters(lambda: true)
# => [[:req, :first], [:req, :second]]
# rubocop:enable Style/Lambda
# return, next and break: where do they go?
def proc_return
proc { return :from_proc }.call
:after
end
def lambda_return
-> { return :from_lambda }.call # rubocop:disable Style/RedundantReturn
:after
end
def block_return = [1, 2, 3].each { |n| return n * 10 if n == 2 }
p proc_return, lambda_return, block_return
# => :from_proc
# => :after
# => 20
p proc { next 7 }.call, -> { next 7 }.call
# => 7
# => 7
p([1, 2, 3].map { |n| n == 2 ? next : n })
# => [1, nil, 3]
def block_break = [1, 2, 3].each { |n| break n * 10 if n == 2 }
p block_break
# => 20
p(-> { break 5 }.call)
# => 5
Source: Proc, Proc#arity, Proc#parameters, Control Expressions: break
The diagram summarises where execution continues after each keyword. "Block or proc" means the code between braces
or do and end given to a method such as each, or a Proc made by proc or Proc.new; "lambda" is any lambda.
LocalJumpError if that method has returned"] BP -->|next| N1["Ends this call of the block
the argument is the block value"] BP -->|break| B1["Ends the method the block was given to
LocalJumpError if that method has returned"] L -->|return| R2["Ends the lambda
the argument is its value"] L -->|next| N2["Ends the lambda
the argument is its value"] L -->|break| B2["Ends the lambda
the argument is its value"]
Orphaned procs and LocalJumpError
A proc’s return and break need their target method to still be running. If a proc outlives the method that created
it, or the method it was given to, the jump has nowhere to go and Ruby raises LocalJumpError. A lambda keeps both
local, so lambdas cannot be orphaned.
# frozen_string_literal: true
def make_proc = proc { return 1 }
def make_break_proc = proc { break 5 }
[-> { make_proc.call }, -> { make_break_proc.call }].each do |attempt|
attempt.call
rescue LocalJumpError => e
puts "LocalJumpError (#{e.reason}): #{e.message}"
end
# prints: LocalJumpError (return): unexpected return
# prints: LocalJumpError (break): break from proc-closure
# A lambda keeps return local, even when it outlives the method that built it.
def make_lambda = -> { return 1 } # rubocop:disable Style/RedundantReturn
p make_lambda.call
# => 1
Source: LocalJumpError, LocalJumpError#reason
Practical consequences:
-
Do not store a
procthat containsreturnand call it later. Use a lambda, ornext. -
Storing a block as a callback (
@callback = block) and calling it later is fine, but areturnorbreakin it fails the same way. Callback blocks should usenextfor early exit. -
A
returnin a proc that runs in another thread raisesLocalJumpError("unexpected return"), because the target method is not on that thread’s stack; the thread also prints the usual "terminated with exception" report. -
LocalJumpErroris also whatyieldraises when there is no block, withreason:noreason. It is aStandardError, so a barerescuecatches it; see Exceptions and Error Handling for the hierarchy. -
returnat the top level of a script, even inside a block or proc, simply ends the script (a top-levelreturnis allowed since Ruby 2.4); this surprises people who paste a snippet into a file.
arity, parameters and source_location
Proc#arity returns the number of required arguments, or -n-1 when there are n required arguments and optional ones.
It differs between procs and lambdas for optional parameters: proc { |a, b = 1| }.arity is 1 while the same lambda is
-2, and lambda { |a:| }.arity is 1 (a required keyword counts as one argument) while lambda { |a: 1| }.arity is
-1. Proc#parameters returns the list of [kind, name] pairs, where kind is :req, :opt, :rest, :keyreq, :key,
:keyrest or :block; a proc reports its positional parameters as :opt because it does not require them, and
parameters(lambda: true) shows what the lambda reading would be. source_location gives [file, line] and
Proc#inspect includes it and a (lambda) marker, which is the quickest way to find where an anonymous callable came from.
Proc#ruby2_keywords exists only for backward compatibility with code that forwards keywords through *args;
the argument-forwarding page explains why ... replaces it.
Closures
A proc remembers the variables of the scope where it was written, not copies of their values. Several procs created in the same scope share the same variables, a proc created in a method keeps that method’s locals alive after the method returns, and a later assignment to a captured variable is visible to the proc.
# frozen_string_literal: true
# Each call to counter closes over its own variable.
def counter(start = 0)
count = start
increment = -> { count += 1 }
peek = -> { count }
[increment, peek]
end
increment, peek = counter(10)
increment.call
increment.call
p peek.call
# => 12
other_increment, other_peek = counter
other_increment.call
p other_peek.call, peek.call
# => 1
# => 12
# The closure's binding is an object you can inspect and even change.
limit = 3
check = ->(n) { n <= limit }
scope = check.binding
p scope.local_variable_defined?(:limit)
# => true
p scope.local_variable_get(:limit)
# => 3
scope.local_variable_set(:limit, 1)
p check.call(2)
# => false
p scope.receiver
# => main
# for shares one variable between iterations; each gives every pass its own.
shared = []
for i in 1..3 do shared << -> { i } end # rubocop:disable Style/For
p shared.map(&:call)
# => [3, 3, 3]
separate = (1..3).map { |n| -> { n } }
p separate.map(&:call)
# => [1, 2, 3]
# A proc remembers where it came from.
p(check.source_location.then { |file, line| [File.basename(file), line] })
# => ["ex09_closures.rb", 24]
p check.inspect.sub(/0x\h+/, "0x...").sub(/ \S+:/, " FILE:")
# => "#<Proc:0x... FILE:24 (lambda)>"
# A closure captures variables, not values.
rate = 2
double = ->(n) { n * rate }
rate = 3
p double.call(5)
# => 15
Source: Proc#binding, Binding, Proc#source_location
What follows from this:
-
Factory functions:
counterreturns two lambdas over the samecount, a lightweight alternative to a class with one instance variable. A lambda closing over mutable state is an object in disguise; when the state grows, switch to a class. -
Binding:
Proc#bindingreturns the captured scope as aBindingobject, withlocal_variable_get,local_variable_set,local_variable_defined?,local_variables,receiverandeval.Kernel#bindingcaptures the current scope. The mechanism behind ERB templates,binding.irband many debuggers. Writing through aBindingchanges the variable the closure sees, as the example shows. -
The
forloop does not make a scope: its variable is shared by all iterations (all three lambdas return3), while a block parameter is fresh for each call. This is the classic closure bug; prefereach,map,timesorupto. -
Memory: a closure keeps the whole captured scope reachable, not just the variables it uses. A long-lived callback created in a method with a big local (a loaded file, a large array) keeps that object alive. Create long-lived lambdas in small methods, or assign big locals to
nilwhen done. See Garbage Collection and Memory. -
Threads: threads started with
Thread.new { ... }share the captured variables, so sharing mutable captured state needs aMutex; see Threads and the GVL. A block passed toRactor.newcannot access outer variables at all; Ruby 4.0 addsRactor.shareable_procandRactor.shareable_lambdato make a proc that can cross a Ractor boundary (see Ractors). -
Constants and instance variables: a block sees
self, instance variables, constants and methods of where it was written, becauseselfis part of the captured context. The next section shows how to change that on purpose.
Blocks that change self: instance_exec and define_method
A block normally keeps the self it was written in. instance_eval, instance_exec, class_eval, class_exec and
define_method run a block with a different self, while the captured local variables stay visible. This is the basis
of nearly every Ruby DSL (RSpec, Rake, Bundler’s Gemfile, the Shelf.configure style): the block reads like
configuration, but runs against an object that interprets each call.
# frozen_string_literal: true
require "shelf"
# A block normally keeps the self it was written in; instance_exec rebinds it.
class Report
def initialize = @lines = []
def title(text) = @lines << text.upcase
def row(text) = @lines << "- #{text}"
def build(&)
instance_exec(&)
@lines
end
end
prefix = "Shelf"
lines = Report.new.build do
title "#{prefix} report"
row "Dune"
end
p lines
# => ["SHELF REPORT", "- Dune"]
# Arguments are passed through, and self is the receiver while locals stay visible.
p "dune".instance_exec(2) { |times| self * times }
# => "dunedune"
Source: BasicObject#instance_exec
define_method is the closure-based way to define a method: because the body is a block, it can use the local
variables around the definition (a loop variable, a configuration value), which a def body never can.
# frozen_string_literal: true
# define_method closes over the surrounding scope, which def never does.
class Loan
%i[available lent overdue returned].each do |state|
define_method(:"#{state}?") { @state == state }
end
define_method(:renew) { |days| "renewed for #{days} days" }
def initialize(state) = @state = state
end
p Loan.new(:lent).lent?, Loan.new(:lent).returned?
# => true
# => false
# A block given to define_method is a lambda-like method body, so arity is checked.
begin
Loan.new(:lent).renew
rescue ArgumentError => e
puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: wrong number of arguments (given 0, expected 1)
Source: Module#define_method
Details to remember:
-
instance_exec(*args, &block)passes arguments to the block;instance_evalpasses the receiver as the only argument. Preferinstance_exec. -
yieldinside adefine_methodbody is aSyntaxError("Invalid yield") when written at class level, and refers to the enclosing method’s block when written inside another method. A method defined withdefine_methodreceives its own block through an explicit parameter:define_method(:each) { |&block| ... }. -
A method defined by
define_methodcan return withreturn, because a method defined from a block has lambda semantics. -
Constants are looked up where the block was written, not in the new
self(Outer.module_eval { X }does not findOuter::X), whiledefinside the block defines instance methods underclass_evaland singleton methods underinstance_eval. The rules are on Hooks, eval and DSLs and Dynamic Dispatch and Method Definition.
Blocks and super
A method that calls super passes its block to the superclass method automatically, whichever form it uses
(bare super, super() or super(a, b)). Only an explicit &nil (super(&nil)) suppresses it, and super(&other) replaces it.
# frozen_string_literal: true
# rubocop:disable Style/OneClassPerFile, Lint/UselessMethodDefinition
# Zsuper (bare super) passes the block along; super() with parentheses does too.
class Base
def each_item = yield(:base)
end
class Child < Base
def each_item
super
end
end
Child.new.each_item { |item| p item }
# => :base
class Silent < Base
def each_item
super(&nil)
end
end
begin
Silent.new.each_item { |item| p item }
rescue LocalJumpError => e
puts "LocalJumpError: #{e.message}"
end
# prints: LocalJumpError: no block given (yield)
# rubocop:enable Style/OneClassPerFile, Lint/UselessMethodDefinition
Source: Methods
(The super rules for arguments and for method lookup are in
Method Lookup and super.)
Method and UnboundMethod
obj.method(:name) returns a Method, a callable that remembers its receiver; Klass.instance_method(:name) returns an
UnboundMethod, the same without a receiver. They are the object form of a method, not a closure over a block: call,
to_proc, arity, parameters, owner, receiver, name, original_name (the name before alias), source_location,
super_method, curry, >>, << and unbind / bind / bind_call cover what you can do with them.
# frozen_string_literal: true
require "shelf"
book = Shelf::Book.new(
isbn: Shelf::Isbn.new(digits: "978-0-441-17271-9"),
title: "Dune",
author: Shelf::Author.new(name: "Frank Herbert")
)
# A Method is a method bound to a receiver.
to_string = book.method(:to_s)
p to_string.call
# => "Dune by Frank Herbert"
p to_string.receiver == book, to_string.owner, to_string.name, to_string.arity
# => true
# => Shelf::Book
# => :to_s
# => 0
p book.method(:title).owner, book.method(:with).owner
# => Shelf::Book
# => Data
p [1, 2, 3].method(:inject).arity, Integer.instance_method(:+).parameters
# => -1
# => [[:req]]
# An UnboundMethod has an owner but no receiver; bind it to any compatible object.
unbound = Shelf::Book.instance_method(:to_s)
p unbound.class, unbound.bind(book).call == to_string.call
# => UnboundMethod
# => true
p unbound.bind_call(book)
# => "Dune by Frank Herbert"
p to_string.unbind.name
# => :to_s
begin
unbound.bind(Object.new)
rescue TypeError => e
puts "TypeError: #{e.message}"
end
# prints: TypeError: bind argument must be an instance of Shelf::Book
begin
book.method(:borrow)
rescue NameError => e
puts "NameError: #{e.message}"
end
# prints: NameError: undefined method 'borrow' for class 'Shelf::Book'
# A Method converts to a lambda, so it can stand in for a block.
p [book].map(&:to_s)
# => ["Dune by Frank Herbert"]
p book.method(:to_s).to_proc.lambda?
# => true
titles = %w[Dune Emma].map(&Shelf::Author.method(:new))
p titles
# => [#<data Shelf::Author name="Dune">, #<data Shelf::Author name="Emma">]
p(book.method(:to_s).source_location.then { |file, line| [File.basename(file), line] })
# => ["book.rb", 5]
Notes:
-
method(:name)finds public, protected and private methods;public_methodonly public ones. A name that does not exist raisesNameError("undefined method 'borrow' for class 'Shelf::Book'"), unlessrespond_to_missing?says the object handles it (then theMethodcallsmethod_missing). -
A
Methodconverts to a lambda (to_proc.lambda?istrue), so it has strict arity:[[1, 2]].each(&method(:foo))withdef foo(a, b)raisesArgumentError. -
bindaccepts only an instance of the owner’s class (or, for a method from a module, any object); anything else raisesTypeError("bind argument must be an instance of Shelf::Book").bind_callbinds and calls in one step without allocating aMethodobject, which is why it is the idiom for calling a method as defined in a specific class even when the receiver overrides it (Kernel.instance_method(:class).bind_call(obj)). -
Method#inspectprints the owner, the receiver class in parentheses when they differ, the parameters and the source location:#<Method: Object(Kernel)#puts(*)>. -
Method#ownerfor an inherited or mixed-in method is the ancestor that defines it (Dataforwith), which makesmethodan introspection tool; see Reflection, Introspection and Tracing. -
Methods with unusual parameters report them in
parameters:[[:req, :a], [:opt, :b], [:rest, :c], [:keyreq, :d], [:key, :e], [:keyrest, :f], [:block, :g]]. Thearityof such a method is-3.
to_proc and the ampersand
The & in a call asks the argument for its to_proc. This single protocol is why &:upcase, &method(:puts) and
&hash work as blocks, and why you can make your own objects usable the same way.
# frozen_string_literal: true
require "shelf"
# Symbol#to_proc calls the method on its first argument.
upcase = :upcase.to_proc
p upcase.call("dune"), upcase.lambda?, upcase.arity, upcase.parameters
# => "DUNE"
# => true
# => -2
# => [[:req], [:rest]]
p [[1, 2], [3, 4]].map(&:sum)
# => [3, 7]
p [1, 2, 3].inject(&:+)
# => 6
# The receiver is the first yielded value; the rest become arguments.
begin
%w[a b].each_with_index.map(&:first)
rescue NoMethodError => e
puts "NoMethodError: #{e.message}"
end
# prints: NoMethodError: undefined method 'first' for an instance of String
begin
upcase.call
rescue ArgumentError => e
puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: no receiver given
# Hash#to_proc and Method#to_proc turn lookups and methods into blocks.
authors = { "Dune" => "Frank Herbert", "Emma" => "Jane Austen" }
p %w[Dune Emma Ulysses].filter_map(&authors)
# => ["Frank Herbert", "Jane Austen"]
p %w[1 2].map(&method(:Integer))
# => [1, 2]
# Any object with to_proc works after &.
class Prefixed
def initialize(prefix) = @prefix = prefix
def to_proc = ->(title) { "#{@prefix}#{title}" }
end
p %w[Dune Emma].map(&Prefixed.new("> "))
# => ["> Dune", "> Emma"]
begin
%w[Dune].map(&Object.new)
rescue TypeError => e
puts "TypeError: #{e.message}"
end
# prints: TypeError: no implicit conversion of Object into Proc
Source: Symbol#to_proc, Hash#to_proc, Method#to_proc, Proc#to_proc
-
Symbol#to_procbuilds a lambda that calls the named method on its first argument with the remaining arguments (:upcase.to_proc.arityis-2). That is why&:symfails when a block receives several values and the first one is not the receiver you meant (theeach_with_index.map(&:first)case above). -
The symbol-proc calls only public methods (
[1].each(&:puts)raises "private method 'puts' called for an instance of Integer"), consistent withpublic_send. -
Hash#to_proc(Ruby 2.3) looks keys up: handy withmap,selectandfilter_map, where an absent key givesnil. -
A class that defines
to_proccan be used after&, which is how value objects, specifications and "predicate objects" are plugged intoselectandmap.to_procshould return aProc(a lambda is best); anything else raisesTypeError("can’t convert Bad to Proc (Bad#to_proc gives Integer)"). -
Symbol#to_prochas been in the language since Ruby 1.8.7. Procs created from symbols are lambdas, which is why theirarityis-2and not1.
Currying and composition
Proc#curry returns a curried proc that collects arguments until it has enough, then calls the original. The number needed
is the arity (for a lambda: the required parameters), or the argument you pass: curry(n). A lambda with a fixed arity rejects
a wrong n with ArgumentError; one with a splat or optional arguments accepts it. Method#curry works the same way.
Proc#>> and Proc#<< (Ruby 2.6, Method versions 2.7) compose callables: f >> g runs f first and then g on its
result; f << g runs g first. The right-hand side can be any object with call (a Proc, a Method or your own
class); anything else raises TypeError ("callable object is expected"). The result is a lambda when the callable that runs
first is one.
# frozen_string_literal: true
add = ->(a, b) { a + b }
p add.curry[1][2], add.curry.call(1).call(2), add.curry.call(1, 2)
# => 3
# => 3
# => 3
add_three = add.curry[3]
p [1, 2].map(&add_three)
# => [4, 5]
variadic = ->(*numbers) { numbers.sum }
p variadic.curry(3)[1][2][3]
# => 6
begin
add.curry(3)
rescue ArgumentError => e
puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: wrong number of arguments (given 3, expected 2)
p proc { |a, b| [a, b] }.curry[1][2]
# => [1, 2]
# >> runs the receiver first, << runs the argument first.
strip = :strip.to_proc
squeeze = ->(text) { text.squeeze(" ") }
title_case = ->(text) { text.split.map(&:capitalize).join(" ") }
normalize = strip >> squeeze >> title_case
p normalize.call(" the left hand of darkness ")
# => "The Left Hand Of Darkness"
p (title_case << squeeze << strip).call(" the left hand ")
# => "The Left Hand"
# Methods compose too, and any object that responds to call works as the other side.
p (method(:Integer) >> ->(n) { n * 2 }).call("21")
# => 42
class Doubler
def call(number) = number * 2
end
p (add_three >> Doubler.new).call(1)
# => 8
p (proc { |x| x } >> proc { |x| x }).lambda?, (->(x) { x } >> proc { |x| x }).lambda?
# => false
# => true
begin
add_three >> 5
rescue TypeError => e
puts "TypeError: #{e.message}"
end
# prints: TypeError: callable object is expected
Source: Proc#curry, Proc#>>, Proc#<<, Method#curry
Currying is useful for partial application (add_three, passed to map above) and for building specialised validators or
formatters from one general lambda. Composition reads best as a pipeline of small named lambdas
(normalize = strip >> squeeze >> title_case), and steps.inject(:>>) folds a list of callables into one. Ruby does not
reorder or optimise a chain, so use it for clarity rather than speed.
Callable objects as interfaces
call is the closest thing Ruby has to a function type: anything that responds to call can be used where a function is
expected, which makes procs, lambdas, Method objects and your own classes interchangeable. Four idioms rest on it.
-
Duck-typed interfaces. A collaborator that needs one behaviour takes a callable instead of a class hierarchy. The Rack web interface is the famous case: an application is any object with
call(env)returning[status, headers, body], and a middleware is a callable that wraps another callable. The example below builds that shape without any web library. -
Chains and pipelines. A list of callables folded with
inject(:>>)(above) orreduceovercall. -
Strategies and configuration. A hash of lambdas, or a lambda stored in a configuration object, replaces a strategy class when the behaviour is one function; see SOLID and Design Patterns for when a class is better.
-
Matchers.
===on a lambda calls it, so lambdas work incase/when, ingrep(array.grep(adult)) and as pinned values in pattern matching (^adult); see Pattern Matching.
# frozen_string_literal: true
require "shelf"
# The Rack contract in miniature: anything with call(env) returning [status, headers, body].
app = ->(env) { [200, { "content-type" => "text/plain" }, ["Hello #{env[:user]}"]] }
# A middleware is a callable that wraps another callable.
class Timing
def initialize(app) = @app = app
def call(env)
status, headers, body = @app.call(env)
[status, headers.merge("x-handled" => "yes"), body]
end
end
stack = Timing.new(app)
p stack.call({ user: "ana" })
# => [200, {"content-type" => "text/plain", "x-handled" => "yes"}, ["Hello ana"]]
# A chain of callables is a fold.
steps = [:strip.to_proc, :downcase.to_proc, ->(text) { text.tr(" ", "-") }]
slug = steps.inject(:>>)
p slug.call(" Left Hand Of Darkness ")
# => "left-hand-of-darkness"
# Callables as strategies and configuration.
Shelf.configure { |config| config.loan_days = 21 }
late_fee = {
flat: ->(days_late) { days_late.positive? ? 1.0 : 0.0 },
daily: ->(days_late) { [days_late, 0].max * 0.25 }
}
p(late_fee.transform_values { |strategy| strategy.call(4) })
# => {flat: 1.0, daily: 1.0}
p Shelf.config.loan_days
# => 21
# === makes lambdas usable in case/when and in pattern matching.
adult = ->(age) { age >= 18 }
age = 30
case age
when adult then puts "adult"
end
person = { age: }
case person
in { age: ^adult } then puts "adult (pin)"
end
# prints: adult
# prints: adult (pin)
Source: Proc#===, Pattern Matching
Design guidance:
-
Prefer a plain object with
callover a lambda once the callable has state, several collaborators or its own tests. A small class is easier to name, document and mock than a lambda stored in a hash. -
The pattern
def self.call(...) = new(...).call(a class-levelcallthat builds the object and runs it) makes a class usable as a callable and as a function-like "service object"; the forwarding syntax is on Calling Methods and Argument Forwarding. -
Give a callable’s parameters real names, and document whether a lambda or a proc is expected. A
procargument’s lenient arity is a frequent source of silently wrong data; a lambda fails early. -
Methodobjects (&method(:name)) let existing methods plug into the same slots without a wrapper lambda.
The shelf Catalog in practice
The shelf Catalog is a small, complete use of the block machinery: each yields when given a block and otherwise
returns an enumerator, uses the anonymous & to forward the block to Array#each, and the Enumerable mixin turns that
one method into map, select, each_with_index, min_by and the rest.
# frozen_string_literal: true
require "shelf"
catalog = Shelf::Catalog.new
{ "Dune" => "9780441172719", "Emma" => "9780306406157" }.each do |title, digits|
catalog.add(Shelf::Book.new(isbn: Shelf::Isbn.new(digits:), title:, author: Shelf::Author.new(name: "Anon")))
end
# Catalog#each yields when given a block and returns an Enumerator otherwise,
# so every Enumerable method (and external iteration) works.
catalog.each { |book| puts book.title }
# prints: Dune
# prints: Emma
p catalog.each.class
# => Enumerator
p catalog.each.next.title
# => "Dune"
p catalog.map(&:title)
# => ["Dune", "Emma"]
p(catalog.each_with_index.map { |book, index| "#{index}: #{book.title}" })
# => ["0: Dune", "1: Emma"]
Source: Enumerable, Enumerator
Extracting block bodies
When a block body grows past a line or two, or is reused, give it a name. Three tools do this, in the order to try:
-
A method (private if internal) and
&method(:name)or a plain call inside the block: the block becomeseach { |loan| renew(loan) }. -
A lambda assigned to a local variable or constant, passed with
&:loans.select(&overdue). -
A callable object when it needs state or tests.
# frozen_string_literal: true
require "shelf"
def book(title, digits)
Shelf::Book.new(isbn: Shelf::Isbn.new(digits:), title:, author: Shelf::Author.new(name: "Anon"))
end
loans = [
Shelf::Loan.new(book("Dune", "9780441172719"), state: :lent),
Shelf::Loan.new(book("Emma", "9780306406157"), state: :overdue),
Shelf::Loan.new(book("Hobbit", "9780261103252"))
]
# Name a non-trivial block body once, then reuse it where a block is expected.
overdue = ->(loan) { loan.state == :overdue }
title_of = ->(loan) { loan.book.title }
p loans.select(&overdue).map(&title_of)
# => ["Emma"]
p loans.reject(&overdue).map(&:state)
# => [:lent, :available]
p loans.partition(&overdue).map(&:size)
# => [1, 2]
puts loans.map(&:describe)
# prints: Dune is lent
# prints: Emma is overdue
# prints: Hobbit is on the shelf
Source: Enumerable#partition, Enumerable#select
Iterators (each, map, select, reject, flat_map, filter_map, take_while, inject) express a loop with the
intent visible; for and while are for algorithms that need explicit index control. Counting loops have iterators as well
(times, upto, downto, step). See
Enumerable and
Idioms, Style and Clean Code.
Pitfalls and diagnostics
| Symptom | Cause | Fix |
|---|---|---|
|
The method was called without a block |
Guard with |
|
A proc’s |
Use a lambda, or |
|
A lambda used as a block receives the arguments of |
Use a proc/block, or give the lambda one parameter for the pair |
|
|
Braces, a local variable, or parentheses |
|
A variable first assigned inside a block is invisible outside it |
Assign it before the block |
Every lambda in a |
|
Use |
|
The symbol-proc takes the first yielded value as receiver and the rest as arguments |
Use an explicit block: |
|
|
Pass |
|
|
Write |
|
|
Use one style per block |
|
|
Take |
|
|
Define |
|
|
Pass a |
Callback sees a changed variable |
Closures capture variables, not values |
Copy the value into a block parameter or a new method argument |
Memory stays high after a method returns |
A long-lived closure keeps its whole scope alive |
Create it in a small method, or drop big locals first |
Style and RuboCop
Default RuboCop settings flag these block-related patterns; the rules summarise the community style:
| Cop | Rule |
|---|---|
|
Braces for single-line blocks, |
|
|
|
|
|
|
|
|
|
Avoid calling |
|
Parenthesise a call that takes another call’s block, such as |
|
Prefer |
|
Numbered parameters only in single-line blocks, at most one |
|
No |
|
A block-local variable that is assigned and never read |
|
Use |
The examples in this page that illustrate a form RuboCop rewrites (Proc.new, lambda { }, .(), ===, for, multiple
numbered parameters) carry a short rubocop:disable comment so that the listings stay clean under the default
configuration. Style/ItBlockParameter and Style/NumberedParameters are "pending" cops, which run with NewCops: enable as in the
shelf configuration. The shelf gem’s own configuration is in the
Style, Linting and Static Analysis page.
Version notes
| Version | Change |
|---|---|
2.6 |
|
2.7 |
Numbered parameters |
3.0 |
|
3.1 |
Anonymous block parameter |
3.2 |
A proc with one positional parameter and keywords no longer auto-splats an array |
3.3 |
|
3.4 |
The |
4.0 |
|
The exact release notes are in the What’s New and Migrating Older Code page.
Where older material is outdated
Most printed and online introductions to Ruby predate several of these changes. When following a book or a blog post that covers blocks, check these points:
| Older text says | Ruby 4.0 |
|---|---|
Numbered parameters ( |
The |
|
It was backported to 1.8.7 and is now an everyday idiom; it is also a lambda |
|
|
|
|
A lambda passed as a block gets the elements of a yielded array as separate arguments |
Lambdas are strict: |
Hash printed as |
|
Method definitions always written |
Endless methods (3.0), value omission in hashes and keywords (3.1) and anonymous |
Examples with |
Assign the result ( |
References
-
Proc (arity, lambda semantics,
itand numbered parameters, curry, composition) -
Method and UnboundMethod
-
Binding (including the Ruby 4.0
implicit_parametersmethods) -
Kernel (
proc,lambda,block_given?,binding,catch,throw) -
Calling Methods and Methods (block argument, block-local arguments,
yield) -
Control Expressions (
break,next,redo) -
RuboCop style cops (
Style/Lambda,Style/Proc,Style/SymbolProc,Style/ItBlockParameter)