21
Understanding Blocks & Procs in Ruby
Blocks in Ruby are fairly easy to understand. A block simply contains chunks of code inside and can be passed into methods. If you're familiar with javascript, then a block is equivalent to an anonymous function.
Blocks can be created in two ways, using {..}
or the do..end
syntax.
{..}
is used for single line blocks, while do..end
syntax is used to encapsulate multiple lines of code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#these two blocks are equivalent and have the same output | |
[1,2,3].select { |num| num.odd? } # => [1,3] | |
[1,2,3].select do |num| | |
if num.odd? | |
num | |
end | |
end # => [1,3] |
Unlike methods, you should never use the return
keyword inside a block unless you have a really good reason for it. Lets explore why..
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#correct | |
def yell_words(words) | |
words.map do |word| | |
word.upcase + "!" | |
end | |
end | |
yell_words(["lion", "tiger", "panther"]) #=> ["LION!", "TIGER!", "PANTHER!"] | |
#Incorrect | |
def yell_words(words) | |
words.map do |word| | |
return word.upcase + "!" | |
end | |
end | |
yell_words(["lion", "tiger", "panther"]) #=> "LION!" |
You see, the return
keyword always belong to methods, so in the second example when words.map
iterates over the array, it instantly returns the first word it encounters and yell_words
ends immediately with the output of "LION!" which is not at all what we were expecting. This is a common pitfall for programming newbies, and I surely have been a victim numerous times.
The last thing I want to point out is a shorthand for writing methods that only take 'one' argument that calls a single method. Lets take a peek:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#example 1
[1,2,3].select { |num| num.odd? } #=> [1,3]
["lion", "tiger", "panther"].map { |str| str.length } #=> [4,5,7]
#example 1 refactored
[1,2,3].select(&:odd?) #=> [1,3]
["lion", "tiger", "panther"].map(&:length) #=> [4,5,7]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#example 1 | |
[1,2,3].select { |num| num.odd? } #=> [1,3] | |
["lion", "tiger", "panther"].map { |str| str.length } #=> [4,5,7] | |
#example 1 refactored | |
[1,2,3].select(&:odd?) #=> [1,3] | |
["lion", "tiger", "panther"].map(&:length) #=> [4,5,7] |
Lets break this down to understand what all of it means. First, we are calling Array.select
and passing in the argument &:odd?
. The :odd?
part is just a symbol that refers to the Integer#odd?
'method', which will be called on every element of the array, and &
is used to convert the 'method' into a Proc(a normal Ruby object) that can be passed into select
.
In Ruby it's not possible to pass methods into other methods so we must convert them to Procs in order to do that.
A proc is a ruby object that contains a block inside. Through procs, we are able to pass blocks of code around or even save them to a variable. See example below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Correct | |
#creates a proc and saves it to a variable | |
square = Proc.new { |n| n * n } #=> #<Proc:0x00007fa8282df348 | |
#calling a proc | |
square.call(3) #=> 9 | |
square.call(10) #=> 100 | |
#'call' is a method on the Proc class that calls the block with the specified arguments | |
#Incorrect | |
square = { |n| n * n } #> SyntaxError: unexpected '}', expecting end-of-input |
Now lets see how we can pass a proc into other methods:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def combine_and_proc(word_1, word_2, prc) | |
word = word_1 + " " + word_2 | |
puts prc.call(word) | |
end | |
yell = Proc.new { |word| word.upcase + "!" } | |
silence = Proc.new { |_| '....' } | |
combine_and_proc("help", "me", yell) #=> "HELP ME!" | |
combine_and_proc("i'm", "coding", silence) #=> "...." |
Notice that we can really alter the behavior of methods by passing in procs that do different things depending on our needs. That's great and all, but can we shorten this a bit? Yes we can!
Ruby allow us to automatically convert blocks into procs. Remember our old friend &
from the block's example, earlier? Lets see how it works:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def combine_and_proc(word_1, word_2, &prc) # '&' automatically converts block to a proc | |
word = word_1 + " " + word_2 | |
puts prc.call(word) | |
end | |
#we call the function and pass the block at the end, which gets | |
#captured as the last parameter of combine_and_proc | |
combine_and_proc("help", "me") { |word| word.upcase + "!" } #=> "HELP ME!" | |
combine_and_proc("i'm", "coding") { |_| '....' } #=> "...." |
It's important to note that since we added &prc
as the third argument of the method, combine_and_proc
must always be called with a block or it will throw an error. The block will be taken by the &prc
argument, and &
will turn the block into a proc. Now when we call puts prc.call(word)
inside combine_and_proc
there will be no errors because prc
was turned into a proc by Ruby automatically.
To wrap up, i'd like to mention one last thing that our friend &
can do for us, and that is to turn a proc into a block. Yes, it might sound confusing but it works both ways. Lets take a look:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#here we can see that select takes in a block | |
[1,2,3].select { |num| num.odd? } #=> [1, 3] | |
#example | |
odds = Proc.new { |num| num.odd? } | |
#incorrect | |
[1,2,3].select(odds) #=> ArgumentError: wrong number of arguments (given 1, expected 0) | |
#correct | |
[1,2,3].select(&odds) #=> [1, 3] |
From the previous example we can gather that select
takes in a block as its last argument. Then, we created the proc odds
and passed it like this [1,2,3].select(odds)
and so we get an error because odds
is a proc and select
is expecting a block. So the correct way would be to call select with &odds)
because &
will turn the proc into a block right before giving it to the select
method.
I hope these examples were helpful, and please feel free to alert me of any corrections to the article.
Happy coding 😃
21