All About Ruby Procs and Lambdas

An exploration

All About Ruby Procs and Lambdas

It has been a while that I wanted to really know what procs and lambdas are in Ruby. So as an excuse to learn it myself, here is an explanation for you, dear reader :)

What are procs and lambdas

To answer this question, we first need to clear a couple of confusions. Those stem from the fact that you will see the term proc used interchangeably for tw<zo concepts, and you will also see lambda used to mean a different concept than proc (which is technically not untrue).

So, what can the term proc refer to:

  1. The general concept of proc. According to the ruby-doc

    A proc object is an encapsulation of a block of code, which can be stored in a local variable, passed to a method or another proc, and can be called.

  2. procs that are specifically non-lambda procs, it is also in this context that lambda is used to mean a different thing than proc.

To be precise lambdas are a type of proc, which can come in two flavours:

  • Lambda procs => a.k.a lambdas
  • Non-lambda procs => a.k.a procs

I hope this clears some confusion around these concepts. It also allows us to set the stage for our exploration.

Untitled Diagram.drawio(2).png

Practical differences between Lambda and Non-Lambda procs

Lambda and non-lambda procs differ in two concrete things:

  1. How they handle break and return keywords
  2. How they handle too many (or not enough) arguments

How non-lambda procs handle break, return and arguments

A non-lambda proc will break and return from inside the method it has been called in. And will throw a LocalJumpError if it breaks or returns outside of a method.

some_proc = proc do
  p 'I am groot'
  return
end

some_proc.call
p 'I will never get to be printed'

=> 'I am groot'
=> LocalJumpError (unexpected return)

def some_method
  some_proc = proc do 
    p 'I am groot' 
   return
end 
some_proc.call

p 'I will never be printed'
end

some_method

p 'but I will'

=> 'I am groot'
=> nil
=> 'but I will'

As for arguments, a non-lambda proc is not strict regarding the number of arguments.

  • If you give it too few, it will assign as many as it can and set the rest to nil:
showcaser =  proc {|arg1, arg2| p "I am arg1: #{arg1} | and I arg2: #{arg2}" }

showcaser.call("groot")

=> "I am arg1: groot | and I arg2: "
  • if you give it too many, it will ignore the arguments at the end
showcaser =  proc {|arg1, arg2| p "I am arg1: #{arg1} | and I arg2: #{arg2}" }

showcaser.call("groot", "black widow", "starlord" )

=> "I am arg1: groot | and I arg2: black widow"

How lambda procs handle break, return and arguments

A lambda proc will break and return from inside the proc it has been called in.

some_proc = lambda do
  p 'I am groot'
  return
end

some_proc.call
p 'I will actually get to be printed'

=> 'I am groot'
=> nil
=> 'I will actually get to be printed'

def some_method
  some_proc = lambda do 
    p 'I am groot' 
   return
  end 
  some_proc.call

  p 'I will actually get to be printed'
end

some_method

p 'I will also'

=> 'I am groot'
=> 'I will actually get to be printed'
=> 'I will also'

As for arguments, a non-lambda proc is strict regarding the number of arguments.

  • If you give it too few, it will throw an argument error:
showcaser =  lambda {|arg1, arg2| p "I am arg1: #{arg1} | and I arg2: #{arg2}" }

showcaser.call("groot")

=> ArgumentError (wrong number of arguments (given 1, expected 2))
  • if you give it too many, it will again throw an argument error
showcaser =  lambda {|arg1, arg2| p "I am arg1: #{arg1} | and I arg2: #{arg2}" }

showcaser.call("groot", "black widow", "starlord" )

=> ArgumentError (wrong number of arguments (given 3, expected 2))

Untitled Diagram.drawio(4).png

What do all Procs have in common?

All procs do share (obviously) some characteristics:

  • They remember the context they were called in:
# This example comes straight from the doc as I found it very explicit

def gen_times(factor)
  Proc.new {|n| n*factor } 
end

times3 = gen_times(3)
times5 = gen_times(5)

times3.call(12)               #=> 36
times5.call(5)                #=> 25
times3.call(times5.call(4))   #=> 60
  • They both serve to store a piece of code to be executed later on the program. Untitled Diagram.drawio(5).png

A cool learning

A cool learning I had from this exploration is that when you use a block of code in Ruby, you create a non-lambda proc that will get called on the method.

An example of that is map


array = [1, 2, 3]

array.map do |el| # <= proc start
  p el # <= proc body
end # <= proc end

=> 1
=> 2
=> 3

On the other hand, lambda procs are very useful as arguments to higher-order functions, as they behave very close to how a method would behave.

Conclusion

I hope this article was as instructive for you as the research has been for me. If you have 15-20 mins to spare, I do recommend that you read through the ruby-doc for Procs