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
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
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
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]
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
Warning: Avoid using
for ... in
in Ruby, prefereach
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]
Note:
STDOUT
represents the standard output, or typically your console screen.=>
represents a return value, andeach
returns the originalArray
. 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]
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' {
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') {
# ...
}
...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
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
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
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]
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]
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]
...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 forto_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]
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]
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]
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
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 yield
s 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]
...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))
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]
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)
For our implicit one:
map_implied([1, 2, 3])
# LocalJumpError (no block given (yield))
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
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>
We'll get into Enumerator
s 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
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
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)
...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
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
...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
Interestingly Lambda Functions are a type of Proc Function, try it yourself:
-> ruby {}
# => #<Proc:0x00007ff3d88c30d8 (irb):231 (lambda)>
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))
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
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
Top comments (4)
Another important difference between
{ }
anddo end
is that{ }
has a higher precedence thando end
. Like the documentation says: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.)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.
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.
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.