#---Proc and Lambda---
Procs and Lambdas are similar to blocks in that they too are anonymous functions.
But blocks have limitations:
Which is why we have procs and lambdas. They are proper object representations of a ruby block.
Here’s how you create a block that’s a proc, using the
proc
method:
one_adder = proc { |x| x + 1 }
p one_adder # #<Proc:0x000000011bd93408 sample2.rb:8>
And here’s how you’ll call this proc that’s assigned to a variable:
p one_adder.call(41) # => 42
You can also call it in these other shorthand ways:
p one_adder.(41) # => 42
p one_adder[41] # => 42
Here’s how you create a block that’s a lambda:
perimeter = lambda { |l, b| 2 * (l + b) }
p perimeter
## #<Proc:0x000000011deb32a0 sample2.rb:11 (lambda)>
p perimeter.(3, 6) # => 18
The words proc and lambda in the examples above
are not ruby keywords. They are methods in the
Kernel
module.
You can use the “stabby lambda” syntax ->() {}
to create a lambda concisely:
perimeter = ->(l, b) { 2 * (l + b) }
p perimeter.(3, 6) # => 18
When a function is passed a block, and if that block needs to be passed as argument to yet another function, then you’ll have to convert the passed in block to a proc.
That’s done by capturing the block in a variable
prefixed with &
in the function’s signature.
Function block_to_proc has no intention of calling the passed in block for its result.
Instead it just wants to pass it as an argument to another function truth_teller.
So it converts the block to a proc and puts it into a variable a_proc_from_a_block with this syntax: &a_proc_from_a_block
def block_to_proc(name, &a_proc_from_a_block)
hi = truth_teller(a_proc_from_a_block)
"block_to_proc says to #{name}: #{hi}"
end
def truth_teller(a_proc)
"#{a_proc.call('Dwayne')}, U can't act"
end
res = block_to_proc('The Rock') do |name|
"hello #{name}"
end
p res
## "block_to_proc says to The Rock: hello Dwayne,
## U can't act"
Let’s say the truth_teller above is re-defined to work with a block instead of a proc, like so:
def truth_teller
"#{yield('Dwayne')}, U can't act"
end
Then, in block_to_proc, you’ll have to convert the
proc back to block before passing it to
truth_teller using the same &
syntax quirk:
def block_to_proc(name, &a_proc_from_a_block)
hi = truth_teller(&a_proc_from_a_block)
"block_to_proc says to #{name}: #{hi}"
end
Procs and Lambdas differ in how they treat the
arguments and the keywords return
and break
.
For the most part, you don’t have to worry about this. Just stick with lambdas if you don’t want to be caught by any surprises.
You can use the lambda?
method to find out if a block
is a proc or a lambda:
p proc { 'hi' }.lambda? # => false
p -> { 'hi' }.lambda? # => true
You can create a specific kind of anonymous function
with the to_proc
method on a symbol.
Example:
to_s_fn = :to_s.to_proc
p to_s_fn
## => #<Proc:0x000000012889dba8(&:to_s) (lambda)>
(Yes it’s weird that you’re creating a lambda
with a method called to_proc
.)
And the lambda created like this always takes an argument. So you can call it like this:
p to_s_fn.call(123) # => "123"
The result of calling this lambda is the result of calling to_s on the supplied argument 123. The lambda just does this: 123.to_s
Which means, :to_s.proc
is just a shortcut for this:
->(x) { x.to_s }
More examples:
even_fn = :even?.to_proc
p even_fn.call(123) # => false
## Same as `123.even?`
upcase_fn = :upcase.to_proc
p upcase_fn.call('ruby examples') # => "RUBY EXAMPLES"
What’s the purpose of this? Why do we need this?
In Ruby, there’s a common pattern where you iterate over an array of items, and then call a specific method on that item.
Like this:
a1 = [11, 22, 33]
a2 = ['harry', 'ron']
p a1.map { |e| e.to_s }
p a1.find_all { |e| e.even? }
p a2.map { |e| e.upcase }
This pattern is so common that Ruby provides a way to shorthand this:
p a1.map(&:to_s) # => ["11", "22", "33"]
p a1.find_all(&:even?) # => [22]
p a2.map(&:upcase) # => ["HARRY", "RON"]
The way this works syntactically is, when you prefix
certain Ruby things with &
, ruby does specific things:
to_proc
method on
the symbol.Wouldn’t it be useful if there’s such a shorthand for passing the array elements as argument to a function?
## Is there a shorthand for this?
a1.each { |e| puts e }
## a1.each(&:puts) doesn't work
Yes, there is!
a1.each(&method(:puts))
Passing a method’s name to method
returns a
Method object, which responds to to_proc. So this
works. But it’s not as common as the previous
examples because, I think, it’s ugly.
A closure is a function that captures the environment it
was created in.
Blocks, procs and lambdas are all closures because
they can access variables from the context they were
created in, and these “bounded-to-closures” variables
will still be available when the closure is passed
around and called later.
Here’s an example:
make_adder is a function that returns, another function. Well, in ruby-speak, it returns a proc:
def make_adder(add_by)
->(x) { x + add_by }
end
num = 2
add_by_x = make_adder(num)
p add_by_x.class # => Proc
You can call this proc like this:
p add_by_x.call(5) # => 7
This add_by_x proc is a closure because it carries around the value ‘2’ we passed to create it.
Keep this aside for a minute now. Let’s now create a new function that takes a function (a proc/lambda/block!) as an argument:
def ask_age(name, fn)
"Hi #{name}, are you #{fn.call(42)}?"
end
Now we can pass the add_by_x we created above, and
ask_age will call it with some value and get some
other value back.
But note that add_by_x still has the value ‘2’
bound to it. We didn’t pass it
as an argument to fn or ask_age, nor does fn have
access to the num variable defined outside the
ask_age function:
p ask_age('Joe', add_by_x) # => "Hi Joe, are you 44?"
Note: ask_age above is defined to expect a proc in its fn parameter. But if it’s designed to take a block, like so:
def ask_age(name)
"Hi #{name}, are you #{yield(42)}?"
end
then while calling ask_age you’ll have to convert
the proc add_by_x to a block by prefixing it with
&
:
p ask_age('Joe', &add_by_x) # => "Hi Joe, are you 44?"
Don’t fuss too much about this. I’ve been doing this for over a decade, and there never was a jira ticket explaining a dramatic problem caused by, or lack of closures. Just learn to use them and move on.