Dissecting Ruby
Enumerable — Implementing Find
Do you ever wonder what is going on behind the scenes when you code? This is something I think about a lot. I am a ruby developer who has depended on different methods in ruby without actually knowing how it is implemented.
I am here to discuss a few of them in detail, or well, dissect some of them. Rather than outline the original C implementation of methods, I’ll try my best to describe it in detail from Ruby’s point of view.
It’s really hard to survive in the Ruby world without using collections and methods on them. One such method is find
. It returns the first element which satisfies the given condition in the block. If there is no block, then it returns the enumerator itself.
marks = [40, 60, 34, 70]
marks.find{|a| a < 40}
=> 34marks_by_subject = {
english: 40, maths: 60, physics: 34, chemistry: 70
}
marks_by_subject.find{|key,value| value < 40}
=> [:physics, 34]divisible_range = 1..5
divisible_range.find{|num| num % 2 == 0 }
=> 2
Like the find
method, there are other common methods every collection has, which originates from the enumerable module. It’s the Ruby way to put the common behavior used by different classes into a module and mixin those modules with classes.
For classes to use the Enumerable module, it has to define an instance method namedeach
. The each
method is supposed to serve the block with each element. If each
method is not defined inside the class, trying to use enumerable methods will throw an NoMethodError
.
To serve the block with each element, we need to use yield. yield
is a keyword in Ruby which can be used to call the block. The return value of yield
is the block’s return value.
class Week
include Enumerable
def each
yield "Monday"
yield "Tuesday"
yield "Wednesday"
yield "Thursday"
yield "Friday"
yield "Saturday"
yield "Sunday"
end
end
week = Week.new
=> #<Week:0x0000559fe6b79a40>
week.include?("Friday")
=> true
week.find {|day| day.start_with?('Sat') }
=> "Saturday"
Even though we have not defined the methods like include?
or find
in the class Week, the Enumerable module and each method helps us to achieve this function.
To understand this better, I thought of implementing the find
and each
method in ruby.
I opened the array class and created my version of each,my_each
. It’s a simple method which goes through each element in an array by index with the help of a while loop and serves it to the block.
class Array
def my_each
return to_enum(:my_each) unless block_given?
i = 0
while i < size
yield self[i]
i += 1
end
self
end
endmarks = [40, 60, 77, 81, 67]
marks.my_each{|mark| p mark} # will print all elements in array
It’s a common practice to return an Enumerator when they’re called without a block. Enumerators can be used to chain methods. In simple, Enumerators are similar to iterators but instead of being a method, it’s an object with a state that can track its own progress. Talking about Enumerators could be an article in itself, so lets not get into that now.
The next step is to create a myfind
method which will get a value from the each
method. This value is then passed to the block which is provided to the my_find
method. If the block returns true, we break from each
and return the element found.
module MyEnumerable
def my_find failure = nil
return to_enum(:my_find) unless block_given?
element_found = nil
is_found = false
each do |num|
if yield num
element_found = num
is_found = true
break
end
end
unless (is_found || failure.nil?)
failure.call
else
element_found
end
end
endclass Array
include MyEnumerable
end
This code has created a MyEnumerable
module which has a myfind
method that is included in the Array
class. The optional failure
argument in the myfind
method will be a lambda/proc object which will be called if the find operation fails.
failure = lambda { "No Distinction" }marks = [40, 60, 77, 81, 67]marks.my_find(failure) {|mark| mark > 90 }
=> "No Distinction"marks.my_find{|mark| mark.equal? 77 }
=> 77
It’s not just arrays, we can use the myfind
method we created in all the classes which have an each method.
class Hash
include MyEnumerable
endmarks_by_subject = {
english: 40, maths: 60, physics: 34, chemistry: 70
}marks_by_subject.my_find{|key,value| value < 40}
=> [:physics, 34]class Week
include MyEnumerable
endweek.my_find {|day| day.start_with?('Sat') }
=> "Saturday"
We can create all the Enumerable methods on top of each method like this.
By creating your own find and each methods, we’re able to recognize that under the hood, all Enumerable methods use the each method. Please let me know what you think, your feedback is appreciated.