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:
- 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.
- procs that are specifically non-lambda procs, it is also in this context that
lambda
is used to mean a different thing thanproc
.
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.
Practical differences between Lambda and Non-Lambda procs
Lambda and non-lambda procs differ in two concrete things:
- How they handle
break
andreturn
keywords - 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 break
s or return
s 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))
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.
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