Ruby Procs & Lambdas

Ruby

Procs and lambdas are commonly confused concepts in the Ruby language. They seemingly serve the same purpose, but have a few significant implementation differences that are crucial to understand.

Similarities

Procs and lambdas are both useful for inserting blocks of code into methods. Often times the use of either produces interchangeable results. Here are some basic examples where Proc and lambda produce identical outcomes:

proc = Proc.new { |x| puts "I am a Proc with a param of #{x}" }
lam  = lambda { |x| puts "I am a Lambda with a param of #{x}" }

(1..10).each(&proc)
(1..10).each(&lam)
# Prints the line 10 times using the numbers from 1 to 10 as the params.

def call_with_block(num, &block)
  block.call(num)
end

def call_with_yield(num)
  yield(num)
end

call_with_block('block', &proc) # prints "I am a Proc with a param of block"
call_with_block('block', &lam)  # prints "I am a Lambda with a param of block"

call_with_yield('yield', &proc) # prints "I am a Proc with a param of yield"
call_with_yield('yield', &lam)  # prints "I am a Lambda with a param of yield"

Procs and lambdas also belong to the same Proc class:

puts proc.class     # returns "Proc"
puts lambda.class   # returns "Proc"

However, Proc creates instances of the Proc class while lambda creates lambda subclasses:

puts proc           # returns "#<Proc:0x007f8c1237e998@(pry):3>"
puts lambda         # returns "#<Proc:0x007f8c1235c870@(pry):4 (lambda)>"

Differences

While the above blocks both produce the same outcome, there are two important distinctions to make between procs and lambdass.

Procs do not check argument lists. Lambdas do.

This means any number of arguments (or no arguments) can be passed to a Proc and it will not raise an exception.

A lambda on the otherhand, requires the correct number of arguments be passed to it, just like regular Ruby methods (note: this method-like behavior will be key in distinguishing the two and will be detailed more below).

proc = Proc.new { |x, y, z| puts [x, y, z].join('') }
lam = lambda { |x, y, z| puts [x, y, z].join('') }

proc.call()               # returns ""
proc.call(1, 2)           # prints "12"
proc.call(1, 2, 3)        # prints "123"
proc.call(1, 2, 3, 4, 5)  # prints "123"

lam.call()                # ArgumentError: wrong number of arguments (0 for 3)
lam.call(1, 2)            # ArgumentError: wrong number of arguments (2 for 3)
lam.call(1, 2, 3)         # prints "123"
lam.call(1, 2, 3, 4, 5)   # ArgumentError: wrong number of arguments (5 for 3)

The return statement has drastically different effects.

Lambda return statements behave in a more controlled manner. Calling return from a lambda will exit out of the lambda process and return any value called with return. This behavior is identical to how ruby methods handle return and should feel very familiar.

Procs do something entirely different. A return inside a Proc, not only exits out of the Proc, but also exits the encapsulating method calling the Proc. This can lead to unintended effects and runtime errors if not properly anticipated (unexpected return is a common error you'll encounter when a Proc is not encapsulated in a function and thus has no function to return out of).

# Print out Fizz Buzz for numbers 1 to n. Rules:
#   If the number is a multiple of 3 print 'Fizz'
#   If the number is a multiple of 5 print 'Buzz'
#   If the number is a multiple of 3 and 5 print 'FizzBuzz'

proc = Proc.new do |num|
	return 'FizzBuzz' if num % 15 == 0
    return 'Fizz' if num % 3 == 0
    return 'Buzz' if num % 5 == 0
    num
end

lam = lambda do |num|
	return 'FizzBuzz' if num % 15 == 0
    return 'Fizz' if num % 3 == 0
    return 'Buzz' if num % 5 == 0
    num
end

def fizz_buzzinator(last_num)
	(1..last_num).each { |num| puts yield(num) }
end

fizz_buzzinator(100, &proc) # Error: unexpected return
fizz_buzzinator(100, &lam)  # Correctly prints out fizz buzz

If you want to get similar lambda-like return behavior in a Proc, use the next statement. This will exit the Proc, with the value next calls, but does not exit out of the encapsulating function.

better_proc = Proc.new do |num|
	next 'FizzBuzz' if num % 15 == 0
    next 'Fizz' if num % 3 == 0
    next 'Buzz' if num % 5 == 0
    num
end

fizz_buzzinator(100, &better_proc)  # Correctly prints out fizz buzz

Takeaway

While the above differences can seem confusing or unexpected remembering this basic generalization will help:

A Proc is a code snippet. A lambda is an anonymous function.

Proc is short for process. Inserting a proc is similar to inserting its block of code directly inline to where the proc is called. It gives a programmer the ability to inject code snippets that can be easily reused. Since these are just blocks of code and not functions, there is no argument checking and returning from the proc behaves like returning from the code calling the proc.

Lambdas on the other hand behave like functions. In other programming languages they are most closely related to anonymous functions. As such, they require the correct number of arguments. Lambdas also return a value, which the calling method can use however it wants.

I generally prefer lambdas as I like the strict argument checking and especially prefer the return value being restricted to the containg scope.

As always though, it's most important to understand the tools and use the right one for the specific job.