Skip to content

Instantly share code, notes, and snippets.

@jules27
Last active October 13, 2021 14:46
Show Gist options
  • Select an option

  • Save jules27/3668380 to your computer and use it in GitHub Desktop.

Select an option

Save jules27/3668380 to your computer and use it in GitHub Desktop.
Using the "end" keyword in Ruby

Introduction

After working through exercises for learning Ruby as part of preparation for the upcoming Dev Bootcamp (note: no longer exists), we had a friendly discussion about the exercise to implement a Reverse Polish notation calculator. At the end of discussion, one of the staff members, Jesse, posted his solution to students.

The solution is well-designed, clean, and easy to read. However, one line of code caught my eye:

tokenize(array).inject([]) do |stack, token|
      <...>
end.pop

You can do what with the end keyword?

I was given some pointers to ponder about, so I spent some time playing with what we can do with end.

Exploring the end keyword

Calling Array methods

First, compare these two array definitions:

some_value = [1,2,3].map do |int|
               int + 10
             end
p some_value         # => [11, 12, 13]
p some_value.reverse # => [13, 12, 11]

some_other_value = [1,2,3].map do |int|
                     int + 10
                   end.reverse
p some_other_value   # => [13, 12, 11]

This is pretty straightforward. some_value is constructed as expected, and #reverse does its job of reversing the resulting array. some_other_value contains end.reverse, and we see that line does essentially the same thing as some_value.reverse.

It appears that you can call array methods on the end keyword, or more accurately, call methods on the object returned by the end keyword, and that method gets called once at the end of the loop. Let's try some more things.

first_element = [1,2,3].each do
                end.first
p first_element      # => 1

last_element = [1,2,3].each do
               end.last
p last_element       # => 3

These results are also pretty straightforward, as #first and #last are both array methods.

Let's try a different notation:

middle_element = [1,2,3].each do
                 end[1]
p middle_element     # => 2

Hey, that's pretty neat. So does end actually return an array?

Calling Object methods

Let's try some methods that are not specific to the Array class.

my_class = [1,2,3].each do
           end.class
p my_class           # => Array

my_id = [1,2,3].each do
        end.object_id
p my_id              # => 70236355699360 (result is different every time)

This confirms that end is an array in this definition, and that it's also possible to call methods from its parent class.

Calling ? and ! methods

What about ? and ! methods? (for lack of better word)

a = [1,2,3]

reversed =  a.each do
            end.reverse
p reversed           # => [3, 2, 1]
p a                  # => [1, 2, 3]

reversed_again = a.each do
                 end.reverse! 
p reversed_again     # => [3, 2, 1]
p a                  # => [3, 2, 1]

am_i_empty = a.each do
             end.empty?
p am_i_empty         # false

While it seems that end.reverse and end.reverse! have the same result, the reverse method has left the variable a untouched, and the reverse! method has modified it.

Daisy-chaining of calling methods using end

It's also possible to continuously call methods using the object returned by end in a daisy-chain fashion.

chained_map = [1,2,3].map do |int|
                 int + 10       # => [11, 12, 13]
               end.map do |int|
                 int + 10       # => [21, 22, 23]
               end.map do |int|
                 int + 10       # => [31, 32, 33]
               end.reverse
p chained_map                   # => [33, 32, 31]

In this example, each iteration increases each value in the array by 10, so at the end of the third iteration, we get [31, 32, 33]. The last end.reverse reverse that result, so by the last iteration, we get [33, 32, 31].

Checking the returned class type

Most of what I've written so far has been applying the end keyword to the Array object, but we can certainly do the same for other objects.

When I first raised the question, I wondered if the reason that you can call Array methods on the object returned by end is because the starting object (inject([])) is an array. The correct answer is that methods called on the object returned by end operates on the ending object, not the starting object.

type_test = [1,2,3].inject(Hash.new(0)) do |result, element|
              result 
            end.class
p type_test           # => Hash

type_test_again = [1,2,3].inject(Hash.new(0)) do |result, element|
                    result = element
                  end.class
p type_test_again     # => Fixnum

Here we see that even though both definitions have the same starting object Hash, the resulting classes are different.

In the first definition, type_test has the resulting value of {}, an empty hash, and thus returns the class Hash. The second definition type_test_again has the resulting value of 3, so it returns the class Fixnum.

(Thanks to Arne for pointing this out!)

Conclusion

I never imagined that you can call methods on the end keyword, or more precisely, on the object returned by the end keyword. Jesse says: every valid Ruby expression returns something. You can assign that something to a variable, call methods on it, etc.

I hope this gist was helpful to some of you. Thanks for reading!

@Samuelfaure
Copy link

Thanks for this helpful review

@pravinchandar
Copy link

You can do what with the end keyword?

LOL, exactly my thought when I first saw it.

@guysbryant
Copy link

For anyone finding this from the future:
You get the same behavior by chaining method calls onto the closing brace too.

some_other_value = [1,2,3].map do |int|
                     int + 10
                   end.reverse

is the same as

some_other_value = [1,2,3].map{|int| int + 10}.reverse

@jules27
Copy link
Author

jules27 commented Apr 7, 2021

Omg, I wrote this 9 years ago (as of today) and completely forgot about it, not to mention being totally unaware of comments made on this gist ever since. Glad people have found it useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment