Blocks, Procs, and Lambdas
Ruby has three closely related callable forms: blocks (one-shot, attached to a method call), `Proc` objects (named, non-strict about arity), and `Lambda` objects (named, strict about arity and `return` semantics). This snippet shows how each is constructed, when `yield` versus `&block` is appropriate, and the subtle differences in `return` and arity checking that decide which to use.
801 views
4
# A block is the do/end or {} attached to a method call.
# Inside the method, `yield` invokes it. `block_given?` checks for one.
def each_squared(arr)
return enum_for(:each_squared, arr) unless block_given?
arr.each do |n|
yield n * n
end
end
each_squared([1, 2, 3]) { |sq| puts "square=#{sq}" }
# Without a block, the method returns an Enumerator (chainable).
squared_enum = each_squared([4, 5, 6])
p squared_enum.to_a # [16, 25, 36]Every Ruby method can implicitly accept ONE block; you do not declare it in the parameter list. Inside the method body, yield value calls the block with value; block_given? lets you behave differently when the caller did not pass one. Returning enum_for(:method_name, ...) when there is no block is the canonical pattern: it lets callers either consume the iterator inline OR get back an Enumerator they can chain with .lazy.map.first(...). This is exactly how built-in methods like Array#map behave.
# Prefix a parameter with & to convert the block into a Proc you can pass on.
def pipeline(&block)
result = block.call(10)
puts "result=#{result}"
end
pipeline { |n| n * 3 } # result=30
# Conversely, & in front of a Proc passes it AS the block to another method.
doubler = ->(n) { n * 2 }
p [1, 2, 3].map(&doubler)
# &:method symbol-to-proc shorthand: equivalent to ->(x) { x.method }
p ["hi", "there"].map(&:upcase)Capturing the block with &block materialises it as a Proc you can store, pass to another method, or call later. The reverse direction, m(&proc_obj), passes a proc back AS the block. The &:method form is the special case for symbols: Ruby calls Symbol#to_proc to produce a one-arg proc that sends the named method. Use this whenever the block body is { |x| x.method }. Capturing with &block is slightly slower than yield because it allocates the proc; reach for yield when you do not need to forward the block.
# Proc.new (or lambda) creates a callable object.
# Procs are LENIENT about arity; lambdas are STRICT.
p_proc = Proc.new { |a, b| [a, b] }
lam = ->(a, b) { [a, b] }
p p_proc.call(1) # [1, nil] -- missing args become nil
p p_proc.call(1, 2, 3) # [1, 2] -- extras are dropped
begin
p lam.call(1)
rescue ArgumentError => e
puts "lambda strict: #{e.message}"
end
# `return` inside a proc returns from the ENCLOSING METHOD;
# `return` inside a lambda only returns from the lambda itself.
def test_return_proc
p_in = Proc.new { return :from_proc }
p_in.call
:unreachable
end
def test_return_lambda
l_in = ->() { return :from_lambda }
l_in.call
:reached
end
puts "proc result: #{test_return_proc.inspect}" # :from_proc
puts "lambda result: #{test_return_lambda.inspect}" # :reachedProcs and lambdas are both Proc instances but behave differently in two important ways. First, lambdas check argument arity strictly and raise ArgumentError on mismatch, while procs silently fill missing args with nil and drop extras. Second, return inside a proc returns from the enclosing method (because procs are 'inline' continuations of the caller), while return inside a lambda only exits the lambda. Default to lambdas for anything that gets stored or passed around as a value; reach for procs only when you specifically want the looser arity or the return-from-method behaviour. Blocks are arity-lenient like procs but cannot be reified without &.
