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.popYou 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.
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 # => 3These 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 # => 2Hey, that's pretty neat. So does end actually return an array?
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.
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 # falseWhile 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.
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].
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 # => FixnumHere 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!)
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!
thanks. it was really helpful. Ruby is rather a strange language still, but this will help a great deal in demystifying it