Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
February 7, 2021 05:29 am GMT

Understanding Ruby - Blocks, Procs, and Lambdas

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)// => 3adder(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 + badder(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_listend
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 vend# 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  # ...enddescribe '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)# => 3adder.(1, 2)# => 3adder[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 vend# 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_listendmap([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_listendmap_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 3endone_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 implementationend
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)# => 5adds_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)endsome_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)endsome_method(1,1)# => 8some_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


Original Link: https://dev.to/baweaver/understanding-ruby-blocks-procs-and-lambdas-24o0

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To