DEV Community

loading...
Cover image for Understanding Ruby - Blocks, Procs, and Lambdas

Understanding Ruby - Blocks, Procs, and Lambdas

baweaver profile image Brandon Weaver ・11 min read

Introduction

Ruby is a language that uses multiple paradigms of programming, most usually Object Oriented and Functional, and with its functional nature comes the idea of functions.

Ruby has three main types of functions it uses: Blocks, Procs, and Lambdas.

This post will take a look at all of them, where you might find them, and things to watch out for when using each of them.

The more you've used languages like Java and C++ the less likely you are to have encountered some of these ideas, but if you've spent time around Javascript a lot of this is going to look very familiar.

Difficulty

Foundational

No prerequisite knowledge needed. This post focuses on foundational and fundamental knowledge for Ruby programmers.

Blocks, Procs, and Lambdas

To start out with it should be noted that each of these three concepts are anonymous functions. In fact I've tended to call them Block Functions, Proc Functions, and Lambda Functions to remind myself there's not really anything special about them beyond just being functions which act slightly different.

That all said, what exactly is a function, and why do we care about them in Ruby?

The Idea of Functions

What is an Anonymous Function?

Why anonymous? In Javascript the difference is that one has a name and the other doesn't:

const anonymousAdder = function (a, b) { return a + b; }
function adder(a, b) { return a + b; }

anonymousAdder(1, 2)
// => 3

adder(1, 2)
// => 3
Enter fullscreen mode Exit fullscreen mode

In Ruby we don't really have an idea of named functions as much as methods:

def adder(a, b) = a + b

adder(1, 2)
# => 3
Enter fullscreen mode Exit fullscreen mode

Note: The above is the one-line method syntax introduced in Ruby 3.0, which is great for short methods which focus on returning results and are on the shorter side.

...and for anonymous functions we have things like lambdas:

adder = -> a, b { a + b }

adder.call(1, 2)
# => 3
Enter fullscreen mode Exit fullscreen mode

Note: This syntax, for reference, is known as "stabby lambda". The arguments are to the right of the arrow and the body of the function is between the braces. Return values are implied from the last expression evaluated, a + b in this case.

When does this come up in Ruby? Well it turns out a lot, because Block Functions are also anonymous. Consider map, the idea of applying a function to every item in a list and returning a new list:

[1, 2, 3].map { |v| v * 2 }
# => [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

This allows us to express the idea of doubling every element in a list in a concise way. If we were to write that in the same way as Java or C might before modern versions introduced map and other functional concepts were introduced, it'd look more like this:

def double_items(list)
  new_list = []

  for item in list
    new_list << item * 2
  end

  new_list
end
Enter fullscreen mode Exit fullscreen mode

Warning: Avoid using for ... in in Ruby, prefer each which we will mention in a moment.

The above anonymous function allows us to abstract the entire idea of transforming elements into one line, and with that comes a substantial amount of power.

We won't get into all the fun things we can do with functions in this article, but rest assured we'll be covering it soon.

Where Are Functions Used?

In Ruby all over the place. Consider each, the way Ruby prefers to go over each element of a list:

[1, 2, 3].each { |v| puts v }

# STDOUT: 1
# STDOUT: 2
# STDOUT: 3
# => [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

Note: STDOUT represents the standard output, or typically your console screen. => represents a return value, and each returns the original Array. I comment these out (# for comment) just in case you copy that while trying out code.

That right there? Well that's a Block Function. It goes over every element of a list and gives that value to the function with the name of v. Function arguments are put in pipes (|) and separated by commas (,) if there are many of them. The function itself is surrounded in brackets ({}).

In this particular function we're outputting the value of v to STDOUT.

You might also see a Block Function look like this:

[1, 2, 3].each do |v|
  puts v
end

# STDOUT: 1
# STDOUT: 2
# STDOUT: 3
# => [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

They both do the same thing.

Nuances of Braces vs Do / End

Typically in Ruby people prefer to use {} for one-line functions and do ... end for multi-line functions. Myself? I tend to prefer the Weirich method instead which signifies that {} is used for functions whose primary role is to return a value, and do ... end is to execute side effects.

Either is fine, but be sure to stay consistent in whichever one you choose in your codebase, and even more-so follow the semantics and rules of established codebases rather than impose your own opinions on them.

Functional Differences and Syntax Issues

There are some differences with parenthesis and how each evaluates them which you may run into. Consider RSpec code here:

describe 'something fun' do
  # ...
end

describe 'something fun' {
  # ...
}
# SyntaxError (1337: syntax error, unexpected '{', expecting end-of-input)
# describe 'something fun' {
Enter fullscreen mode Exit fullscreen mode

The second will syntax error while the first will work just fine. That's because to the second it's ambiguous whether that's a Hash argument or a function, causing Ruby to throw an error. If you put parens around 'something fun' it'll work just fine:

describe('something fun') {
  # ...
}
Enter fullscreen mode Exit fullscreen mode

...but most prefer do ... end for RSpec code, as do I.

Calling a Function

So how does one call a function versus a method? Well there are a few ways, and we'll focus again on Lambda Functions here:

adder = -> a, b { a + b }

adder.call(1, 2)
# => 3

adder.(1, 2)
# => 3

adder[1, 2]
# => 3
Enter fullscreen mode Exit fullscreen mode

There are a few others like === that only work with one-argument functions unless you do some really nasty things like this:

adder === [1, 2]
# ArgumentError (wrong number of arguments (given 1, expected 2))

adder === 1, 2
# SyntaxError ((irb):157: syntax error, unexpected ',', expecting `end')

adder.===(*[1, 2])
=> 3
Enter fullscreen mode Exit fullscreen mode

I would not suggest using that, nor would I suggest explicitly using === like this either. If you want more information on === consider giving Understanding Ruby - Triple Equals a read.

There's one last way to call a function, yield, but we'll save that for the moment and explain it along with the next section on blocks.

Function Arguments

Now here's an interesting one that isn't mentioned very often: All valid method arguments are also valid function arguments:

def all_types_of_args(a, b = 1, *cs, d:, e: 2, **fs, &fn)
end
Enter fullscreen mode Exit fullscreen mode

That means you can very validly do this:

lambda_map = -> list, &fn {
  new_list = []
  list.each { |v| new_list << fn.call(v) }
  new_list
}

lambda_map.call([1, 2, 3]) { |x| x * 2 }
# => [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

Think how that might be fun with default arguments, and especially keyword arguments, as a fun little experiment potential for later.

Ampersand (&) and to_proc

You may well see code like this in Ruby:

[1, 2, 3].select(&:even?)
# => [2]
Enter fullscreen mode Exit fullscreen mode

Searching for & can be difficult, so let's explain it here. & calls to_proc on whatever is after it. In the case of a Symbol here it calls Symbol#to_proc (the to_proc method on an instance of the Symbol class.)

It effectively generates code like this:

[1, 2, 3].select { |v| v.even? }
# => [2]
Enter fullscreen mode Exit fullscreen mode

...where the Symbol that's coerced into a Proc Function acts like a method to be called on whatever is passed into the function. For select that would be the numbers 1, 2, 3.

This also tells Ruby to treat this argument as a Block Function for the sake of passing it to the underlying method, which we'll get to in a moment in our section on Block Functions.

Note: & is syntactic sugar for to_proc, but does not work outside of this context. You can't do this for instance: age_function = &:age. It will result in a Syntax Error.

Types of Functions

Now that we have some groundwork laid, let's take a look at the three types of functions: Block functions, Proc Functions, and Lambda functions.

Note: Technically method is another type of function, but we'll skip that section for this article and save it for a more detailed writeup later on Ruby methods.

Block Functions

The first is likely the most familiar, and most likely to show up in your day to day Ruby code: The Block Function.

As you saw earlier with each it takes a Block Function:

[1, 2, 3].each do |v|
  puts v
end
# STDOUT: 1
# STDOUT: 2
# STDOUT: 3
# => [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

How do we define a function which takes a Block Function like this? Well let's take a look at a few ways.

Explicit Block Functions

The first way is to explicitly tell Ruby we are passing a function to a method:

def map(list, &function)
  new_list = []
  list.each { |v| new_list << function.call(v) }
  new_list
end

map([1, 2, 3]) { |v| v * 2 }
# => [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

Functions are prefixed by &. Frequently you will see this called &block, but for me I tend to prefer &function to clarify the intent that this is a function I intend to use. Frequently I abbreviate it as &fn, but that's just my preference.

For this new map method we're iterating over each item in the list and putting those items into the new_list after we call function on each of them to transform the values. After that's done we return back the new_list at the end.

My preference is for explicit functions because they let me know from the arguments of the method that it takes a function.

Implicit Block Functions

The next way is implied, and uses the yield keyword:

def map_implied(list)
  new_list = []
  list.each { |v| new_list << yield(v) }
  new_list
end

map_implied([1, 2, 3]) { |v| v * 2 }
# => [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

yield is interesting here as it can be present any number of times in a method. In this case we only want it mentioned once in our iteration of the original list. We could technically do this too:

def one_two_three
  yield 1
  yield 2
  yield 3
end

one_two_three { |v| puts v + 1 }
# STDOUT: 2
# STDOUT: 3
# STDOUT: 4
# => nil
Enter fullscreen mode Exit fullscreen mode

Though I have not found a use for this myself in my code.

yield is a keyword that yields a value to the implied function the method was called with. Once it runs out of yields it stops calling the function.

Personally I do not like this as it's more confusing to me than the above, but you may well see this pattern in other Ruby code, so it's good to know it exists.

Ampersand (&) and Block Functions

So how does Ruby know this is a function to pass to a method? In this case it's implied:

map([1, 2, 3]) { |v| v * 2 }
# => [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

...but in this case with a lambda not quite so much:

add_one = -> a { a + 1 }

map([1, 2, 3], add_one)
# ArgumentError (wrong number of arguments (given 2, expected 1))
Enter fullscreen mode Exit fullscreen mode

It's treated as an additional argument unless we prefix it with & to tell the method that this is a Block Function it needs to treat as such:

add_one = -> a { a + 1 }

map([1, 2, 3], &add_one)
# => [2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Block Given and the Missing Block

So what happens if we forget the Block Function then? In the case of our explicit style:

map([1, 2, 3])
# NoMethodError (undefined method `call' for nil:NilClass)
Enter fullscreen mode Exit fullscreen mode

For our implicit one:

map_implied([1, 2, 3])
# LocalJumpError (no block given (yield))
Enter fullscreen mode Exit fullscreen mode

We can guard against this by checking if a Block Function has been given to the method:

def map(list, &fn)
  return list unless block_given?

  # ...rest of implementation
end
Enter fullscreen mode Exit fullscreen mode

In this case if we forget the Block Function it returns the initial list. For Ruby itself it returns an Enumerator instead:

[1, 2, 3].map
# => #<Enumerator: [1, 2, 3]:map>
Enter fullscreen mode Exit fullscreen mode

We'll get into Enumerators another day though.

Proc Functions

The next type of function we'll look into are Proc Functions. You may have noticed to_proc before, but perhaps amusingly you could return a Lambda Function from that.

It should be noted that Proc Functions are the base for Lambda Functions, and why I'm covering them first.

A Proc Function can be defined in a few ways:

adds_two = Proc.new { |x| x + 2 }
adds_two.call(3)
# => 5

adds_three = proc { |y| y + 3 }
adds_three.call(2)
  # => 5
Enter fullscreen mode Exit fullscreen mode

Behavior with Arguments

What's interesting about a Proc Function is that they don't care about the arguments passed to them:

adds_two = Proc.new { |x| x + 2 }
adds_two.call(3, 4, 5)
# => 5
Enter fullscreen mode Exit fullscreen mode

The only reason it would is if the first argument is missing, and only because that causes an error inside the function itself. They can be quite lazy like that. This is one crucial reason why I tend to prefer Lambda Functions, but we'll get into that in a moment.

Behavior with Return

This is another interesting case. If we use return inside of a Proc Function it can do some bad things:

adds_two = Proc.new { |x| return x + 2 }
adds_two.call(3, 4, 5)
# LocalJumpError (unexpected return)
Enter fullscreen mode Exit fullscreen mode

...but if we did that inside of a method instead:

def some_method(a, b)
  adds_three_unless_gt_three = proc { |v|
    return v if v > 3
    v + 3
  }

  adds_three_unless_gt_three.call(a) +
  adds_three_unless_gt_three.call(b)
end

some_method(1, 1)
# => 8, or 4 + 4, or (1 + 3) + (1 + 3)

some_method(5, 5)
# => 5
Enter fullscreen mode Exit fullscreen mode

return actually broke out of the method itself instead of returning a value from the function. If we wanted behavior like return without this we could use next instead, but this is another reason I tend to prefer Lambda Functions.

Lambda Functions

The next type of function we'll look into are Lambda Functions. To start with remember a Lambda Function looks something like this:

adds_one = -> a { a + 1 }
adds_one.call(1)
# => 2
Enter fullscreen mode Exit fullscreen mode

...but it can also look like this, though it's much rarer syntax to see:

adds_three = lambda { |z| z + 3 }
adds_three.call(4)
# => 7
Enter fullscreen mode Exit fullscreen mode

Interestingly Lambda Functions are a type of Proc Function, try it yourself:

-> ruby {}
# => #<Proc:0x00007ff3d88c30d8 (irb):231 (lambda)>
Enter fullscreen mode Exit fullscreen mode

Note: I keep using different argument names as a reminder that the names are arbitrary and could be anything as long as it's a valid variable name. If you want another interesting fact any valid method argument is also a valid argument to any function.

Behavior with Arguments

Lambda Functions, unlike Proc Functions, are very strict about their arguments:

adds_four = -> v { v + 4 }
adds_four.call(4, 5, 6)
# ArgumentError (wrong number of arguments (given 3, expected 1))
Enter fullscreen mode Exit fullscreen mode

They behave much more like methods in this, and can lead to less confusing errors later.

Behavior with Return

Lambda Functions will treat return as a local return rather than trying to return from the outer context of the method:

def some_method(a, b)
  adds_three_unless_gt_three = -> v {
    return v if v > 3
    v + 3
  }

  adds_three_unless_gt_three.call(a) +
  adds_three_unless_gt_three.call(b)
end

some_method(1,1)
# => 8
some_method(5,5)
# => 10
Enter fullscreen mode Exit fullscreen mode

That means both executions of the function will return the value 5 instead of returning 5 for the entire function.

Wrapping Up

This was a fairly broad overview of the types of functions in Ruby, but does not get too much into why you'd want to use them. Rest assured there will be future posts covering this as well.

In Ruby there are several ways to do one thing, and because of that it's useful to know what syntax does what, especially early on. Personally I prefer to pare down the syntax in certain areas, like I tend to use Lambda Functions almost exclusively over Proc Functions, and can't think of a case where I would need to use them instead.

The real fun of functions starts when you start seeing what you can do with them. The following posts on Enumerable and Functional Programming will be very interesting on that note, but until then that's all I have for today.

Want to keep up to date on what I'm writing and working on? Take a look at my new newsletter: The Lapidary Lemur

Discussion (4)

Collapse
3limin4t0r profile image
3limin4t0r • Edited

Another important difference between { } and do end is that { } has a higher precedence than do end. Like the documentation says:

do end has lower precedence than { } so:

method_1 method_2 {
  # ...
}

Sends the block to method_2 while:

method_1 method_2 do
  # ...
end

Sends the block to method_1.

The above is only relevant when omitting parentheses, but is still useful to know.

Jumping ahead to the Proc behavior with arguments, another thing that might be useful to know is that objects that have a #to_ary method (only arrays in core) will automatically be spread (splat) across the argument list if it is the sole argument to the proc. (Similar to what blocks do when an array is passed as the sole argument.)

pair = [3, 4]

proc_add = proc { |a, b| a + b }
proc_add.(pair) #=> 7 # auto spread/splat across proc arguments
proc_add.(*pair) #=> 7

lambda_add = ->(a, b) { a + b }
lambda_add.(pair) #=> ArgumentError (wrong number of arguments (given 1, expected 2))
lambda_add.(*pair) #=> 7

pair.then { |a, b| a + b } #=> 7 # auto spread/splat across block arguments
Enter fullscreen mode Exit fullscreen mode
Collapse
baweaver profile image
Brandon Weaver Author

I seem to recall mentioning the first item somewhere, if not in this article. The second I always tend to forget, and there was one point that that behavior was broken for Lambdas and I had to use procs instead.

Collapse
dmuth profile image
Douglas Muth

This was super helpful, as it help me understand the differences between some of these concepts. I jumped right in by trying to learn Rails, and your blog posts are good for helping me pick up the Ruby part of that. :-)

Thank you for writing this series.

Collapse
thorstenhirsch profile image
Thorsten Hirsch

It's a joy to read your articles, Brandon. Blocks, Procs, and Lambdas are a very good topic for a foundational Ruby series. I just don't understand why you started the series with triple equals. I mean you warn against their usage yourself and you recommend methods with clearer names instead... well, I would have moved such a topic into the appendix.

Forem Open with the Forem app