lambda and Proc - any difference?

Subscribe to receive new articles. No spam. Quality content.

Hey! In previous article we discussed blocks and today I'm going to describe lambdas and Procs. We will understand similarities and difference between them.

lambda

Generally speaking there is nothing hard in Procs and lambdas. Lambda is just a function without a name. It's so called anonymous function.

As we know - almost everything in Ruby is an object. So lambda is an object as well. It's an object which has method call which calls anonymous function. Example:

hello = lambda { puts "Hello" }
hello.call # => Hello

Looks simple, right? We "saved" block of code into hello variable and called it by using hello.call when we needed. We can pass params to lambda:

hello = lambda { |word| puts word }
hello.call("Hello") # => Hello

If we call that lambda without param, Ruby will throw an error:

hello = lambda { |word| puts word }
hello.call("Hello") # => Hello
hello.call # => wrong number of arguments (0 for 1) (ArgumentError)

We see that lambda behaves as a regular method and if we don't pass all required arguments it will raise an error ArgumentError: wrong number of arguments.

Lambda behaves as a regular method inside other methods as well. Check the following example:

def say_hi
  l = lambda { "Hi from lambda" }

  puts "Lambda will say hi:"
  puts l.call
  puts "It worked!"
end

say_hi

# Lambda will say hi:
# Hi from lambda
# It worked!

As we can see lambda's call didn't interrupt execution of say_hi function. Lambda just returned its value and code of say_hi continued to execute.

Another important thing - lambda keeps all scope where it was declared. In our example that means that all variables that was declared before lambda (inside method say_hi) will be available inside lambda:

def say_hi
  x = 10
  l = lambda { "I see x = #{x}" }
  l.call
end

puts say_hi # => I see x = 10

Because lambda has an access to scope's variables - it can change them as well. But variables which declared inside lambda - gets destroyed right after execution and don't leak into global scope.

def count
  counter = 1
  l = lambda do
    counter += 1
    lambda_var = 100
  end
  l.call
  puts counter # => 2
  puts defined?(lambda_var) # => nil
end

As we see lambda's call changed counter variable, but variable lambda_var lives only inside lambda and not accessible from outer scope. One thing to remember that Procs have the same approach of work with scoping.

We can define lambda using keyword lambda or using short version ->:

# without params
l = -> { puts "Hello" }
l = lambda { puts "Hello" }

# with params
l = -> (word) { puts word }
l = lambda { |word| puts word }

Let's go to interesting part. Let's check lambda's class:

puts lambda { |word| puts word }.class # => Proc

Yes, lambda it's just a special case of Proc. So, now we can move forward and learn something new about Procs.

Proc

Proc is a short name of procedure and yes, it's a block of code which we can assign to variable and call when we need it.

# without param
p = Proc.new { puts "Hello" }
p.call # => Hello

# with param
p = Proc.new { |word| puts word }
p.call("Hello") # => Hello

There is also a short version of Proc.new - just proc:

# without param
p = proc { puts "Hello" }
p.call # => Hello

# with param
p = proc { |word| puts word }
p.call("Hello") # => Hello

Procs, as lambdas, keep scope where they've been defined and can change variables:

def say_hi
  x = 10
  p = Proc.new { "I see and can change x too! x = #{x}" }
  p.call
end

puts say_hi # => I see x = 10 too!

Procs and lambdas - difference

Let's consider difference between Procs and lambdas.

1. Lambda strictly checks params number, Proc doesn't

l = ->(a, b) { puts "#{a} and #{b}" }
p = Proc.new { |a, b| puts "#{a} and #{b}" }

Here we have lambda and Proc which accepts two arguments each. Let's try to call lambda with one param:

l.call("foo") # => wrong number of arguments (1 for 2)

Lambda throws an error, because we passed just one argument instead of two.

Now Proc:

p.call("foo") # => foo and

Proc worked. First param it set to "foo", and another to nil. By this example we see that lambdas require all params to be passed. Procs are more flexible with arguments. If we didn't pass some arguments, it will set them to nil.

One thing to know: Procs and lambdas behave as methods and accepts default values for arguments:

l = ->(word = "default") { puts word }
l.call # => default

p = proc { |word = "default"| puts word }
p.call # => default

If we defined default values for arguments, we can call lambda as a Proc and don't pass any values at all. In this case default values will be used.

2. return from lambda just returns value. Explicit return from Proc returns value from current scope (for example method) in which it was defined.

We discussed such example with lambda:

def do_math
  sum = ->(a, b) { return a + b }
  result = sum.call(2, 2)
  "Result of 2+2 is #{result}"
end

do_math # => Result of 2+2 is 4

Even after call sum.call(2, 2), code inside do_math continued to execute and we've seen string with a result. So explicit return from lambda worked as any other return form regular method.

Explicit return from Proc works in different way:

def foo
  p = Proc.new { return "Returned value from Proc" }
  p.call
  "Return from foo"
end

puts foo # => Returned value from Proc

As soon as we called p.call, Proc took control over method foo and after explicit return returned its value and interrupted execution of foo method. We didn't get "Return from do_math" string. Because foo returned Proc's result of execution.

If we want Proc's return to behave as lambda's, we should remove explicit return from Proc:

def foo
  p = Proc.new { "Returned value from Proc" }
  p.call
  "Return from foo"
end

puts foo # => Return from foo

In this case after p.call call, method continued to execute and returned last string from foo method.

Procs and lambdas as blocks

In recent article we talked about passing blocks to methods. Now we know that lambdas and Procs are blocks of code as well. Good question is: is it possible to pass Procs and lambdas like a block inside method? Answer is: YES!

Let's consider example when method reqires string as a param and also can accept a block. If we pass just string - it prints it as is. If we pass block which decorates that string we call it:

def decorate(str)
  block_given? ? yield(str) : str
end

puts decorate("Bare string") # => Bare string
puts decorate("foo") { |str| "Decorated string: #{str}" } # => Decorated string: foo

We could use Proc as a block which we pass into decorate method:

def decorate(str)
  block_given? ? yield(str) : str
end

p = Proc.new { |str| "Decorated string #{str}" }
puts decorate("foo", &p) # => Decorated string: foo

We used &p to pass Proc as a block for method decorate. One important note: block passed as a last parameter.

We can pass lambda as a block as well (for lambdas method to_proc will be called automatically):

func = ->(num) { "#{num}!" }
[1,2,3].map(&func) # => ["1!", "2!", "3!"]

I hope that previous examples were easy to understand. Because if you understand how it works - following example will be easy to understand as well.

I think all of us have seen such code:

["foo", "bar", "baz"].map(&:upcase) # => ["FOO", "BAR", "BAZ"]

This code call method upcase on each element of array. We could write it in this way:

["foo", "bar", "baz"].map { |x| x.upcase }

But developers use short version:

["foo", "bar", "baz"].map(&:upcase)

Let's try to understand all "magic" behind this code. So:

  1. Method map accepts block and passes each element of array one by one.
  2. & tells that we want to pass Proc as a block for map.
  3. We pass symbol :upcase. It's not a Proc, so to_proc method will be called automatically.
  4. Executing :upcase.to_proc creates Proc like this one: proc { |x| x.upcase }
  5. After that Proc is being passed into map method.

That's how we call upcase for each element of an array.

Summary

  1. To simplify: lambdas and Procs - blocks of code which we can assign to variable and call when we need it by call method.
  2. Lambdas strictly control number of arguments which should be passed. Procs don't. If you don't pass all required arguments into Proc it will set them to nil.
  3. If call explicit return from Proc - it will stop execution and return value from that scope where it was defined. Return from lambda behaves the same way as return from regular method.
  4. We can pass Proc as a block using &. If we pass another type, method to_proc will be called automatically.

Subscribe to receive new articles. No spam. Quality content.

Comments