Procs and Lambdas
Maryna Nogtieva
Posted on April 27, 2020
Today I was refreshing my knowledge and learning something new about proc and lambda in Ruby and wanted to share some info about it that, hopefully, might be useful for somebody else.
In the previous post we discussed that almost everything is an object in Ruby except blocks.
Block in Ruby is a very useful feature that helps to remove some repetition in the code and perform various operations on data structures dynamically.
However, sometimes(not too often) we might need to have a way to store a block of code into a variable, which is not possible with regular Blocks in Ruby:
# This will not work and an exception will be thrown
a = { puts "Hello" } # Ruby expects a hash here between curly braces
# syntax error, unexpected keyword_do_block
b = do
puts "Hello"
end
Enter lambdas and procs ✨
Lambda and proc are wrappers around a block that allow us to store a block of code into a variable and use it later. This variable will be an object - an instance of the Proc class.
After a proc is created it can be later passed as an argument to a method.
names = ['Anna', 'Alex', 'Hellie', 'Martin']
proc_name = Proc.new { |name| name.start_with?('A') }
selected_names = names.select(&proc_name)
In the example above we can see only two names printed out
["Anna", "Alex"]
Creating lambda and proc
Proc class has two flavors: proc
s and lambda
s. We will discuss their differences in a moment but first let's review how we can create them. There are few ways both to create and call proc
s and lambda
s:
proc1 = Proc.new { puts "I'm proc1" }
##<Proc:0x00007fce97948b40@(irb):117>
proc2 = proc { puts "I'm proc2" }
##<Proc:0x00007fce9793a180@(irb):118>
lambda1 = lambda { puts "I'm lambda1" }
##<Proc:0x00007fce9792a870@(irb):120 (lambda)>
lambda2 = -> { puts "I'm lambda2" }
##<Proc:0x00007fce9791bbb8@(irb):121 (lambda)>
Stabby lambda ->
is a new syntax for creating lambdas and is used more often than lambda
keyword.
Calling lambda and proc
There are many ways to call both lambda and proc. I personally prefer .call()
and .()
syntax.
proc1.call()
lambda1.call()
I'm proc1
I'm lambda1
proc1.()
lambda1.()
I'm proc1
I'm lambda1
proc1.[]
lambda1.[]
I'm proc1
I'm lambda1
proc1.===
lambda1.===
I'm proc1
I'm lambda1
Difference between lambda and proc
Both lambda and proc are similar on the surface but actually behave quite differently. They are similar in a way that they are instances of the same Proc
class.
irb(main):126:0> proc1.class
Proc < Object
irb(main):127:0> lambda1.class
Proc < Object
But one should not be confused by that fact. We should take a closer look at lambda
and proc
:
-
proc
is not strict with the number of arguments passed
number_of_students = Proc.new do |number|
if number
puts number
else
puts "no students"
end
end
# prints 20
number_of_students.call(20)
# prints "no students"
number_of_students.call()
-
lambda
can scold you in case you pass the wrong number of arguments.
number_of_students2 = ->(number) do
if number
puts number
else
puts "no students"
end
end
# prints 10
number_of_students2.call(10)
# throws an exception: ArgumentError (wrong number of arguments (given 0, expected 1))
number_of_students2.call()
- Keyword
return
has a different behavior: - “return” in lambda behaves like “return” in a method - you call a method from another method, get a return value, and carry on with the code below it.
-
"return" in proc behaves the same way as in a block. If a
proc
withreturn
keyword is called inside a method, then code specified belowproc.call()
will not be executed. The method will return a value that is being returned by theproc
. Note, if you declareproc
withreturn
in the top context and then pass it to a method as an argument, you will get an exceptionLocalJumpError: unexpected return
because we are not allowed to return from top-level context.
Let's look at some examples.
- When a lambda is declared outside of the method(passed to the method as an argument) and when another lambda declared inside a method, we will see all four lines printed out.
def my_lambda(lambda_obj)
internal_lambda = -> { return "in internal lambda" }
puts "before lambda"
puts internal_lambda.call
puts lambda_obj.call
puts "after lambda"
end
lambda_obj = -> { return "in outside lambda" }
my_lambda(lambda_obj)
Output:
before lambda
in internal lambda
in outside lambda
after lambda
- When a proc is declared outside the method in a top-level scope an error will be thrown.
outside_proc = Proc.new { return "From outside proc" }
def my_proc(proc_obj)
puts "before proc"
puts proc_obj.call()
puts "after_proc"
end
my_proc(outside_proc)
Output:
before proc
LocalJumpError: unexpected return
from (irb):98:in `block in irb_binding'
from (irb):102:in `my_proc'
- When proc is declared inside the method, it will be binded to the method scope, and lines that come after calling proc will not be printed out.
def my_proc
proc_obj = Proc.new { return "From inside proc" }
puts "before proc"
puts proc_obj.call()
# this will not be printed out
puts "after_proc"
end
my_proc
Output:
before proc
"From inside proc"
Ruby has functional programming features?
Ruby is one of the purest object-oriented languages but it is spiced up with functional programming flavors. I have some experience with JavaScript and my examples will reference this language for comparison purposes.
- Closures. In JavaScript closure allows the inner function to have access to the lexical scope of the outer function. Procs in Ruby are also bonded to the variable and have access to the context/scope where that variable was declared. Let's see an example:
salary = 1000
bonus = 500
salary_and_bonus = Proc.new { puts "#{salary + bonus}" }
def print_salary_with_bonus(my_proc)
bonus = 700
my_proc.call
end
Will the result of calling #print_salary_with_bonus
method be a 1500
or 1700
?
irb(main):116:0> print_salary_with_bonus(salary_and_bonus)
1500
Because we declared our salary_and_bonus
proc in the outer scope it had access (remembered) to everything else declared in the same context. Therefore, our output printed 1500
.
- Composition. >> operator in Ruby. A composition is an assembly of multiple functions combined together into one function to perform multiple operations on one piece of data. Basically, composition allows us to combine two or more functions into one function.
Proc#>>
was introduced in Ruby 2.6
and it allows us to perform some fun tasks. Let's imagine that we went to an online store and decided to purchase one item from there.
Here is how we would calculate its final price by using a composition:
WHOLESALE_PRICE = 400
base_price = ->(price) { WHOLESALE_PRICE + price }
handling_price = ->(price) { price + price * 0.1 }
total_tax = -> (price) { price + price * 0.13 }
calculate_final_price = base_price >> handling_price >> total_tax
final_price = calculate_final_price.call(100)
# final price => 621.5
Interestingly, Proc#>>
in ruby switches the order of a function call comparing to composition in Haskel or Math.
Usually, when we use a composition the order of data processing goes from right to left f(g(x))
- first we process x
and then g
.
With Proc#>>
we process data from left to right. However, Proc#<<
allows us to process data the same way as it's done in functional programming 🙂 . I suggest checking this blog for more details on that.
Here is a nice article to learn more about composition in Ruby in detail.
- Function currying.
If you learned any functional programming you probably encountered
currying
paradigm before.
With the help of currying, you can take a function with 2 arguments, call this function with one argument. This is called a partial application. Then call the partial application with the final argument to complete the function and evaluate the result.
If you are wondering how currying in ruby can be useful "Applying Higher-order Functions: Partial Application and Currying" part of this article would be a very good read to get more knowledge on that.
Let's try to take the previous example of an item's price calculation and try to use Proc#curry for that purpose:
WHOLESALE_PRICE = 400
calculate_price = ->(method, price, additional_cost) do
price.send(method, additional_cost)
end
add = calculate_price.curry.call(:+)
base_price = add.(WHOLESALE_PRICE, 100)
# => 500
handling_price = add.(base_price, base_price * 0.1)
# => 550.0
tax = handling_price * 0.13
final_price_with_tax = add.(handling_price, tax)
# => 621.5
Thanks for reading, I hope you found this information useful 🙂
Posted on April 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.