Ruby
Functional Eye for the Ruby Guy
The new year is in full swing with February approaching fast and bringing with it the much anticipated release of Ruby 2.0. So it seems like the perfect time to explore a couple of its new features. Two that I found interesting were the Enumerator::Lazy and Refinements so today we'll look at implementing a solution to a Project Euler challenge. This implementation will allow us to utilize these two features without a contrived example. After all, the Ruby 2.0 features are the focus.
Gaining Focus
For those new to Project Euler, it's a site that offers a series of mathematical, or computer programming, challenges to sharpen your problem solving skills. The challenges range in difficulty but since our focus is the language features the problem isn't as important as the solution. Our challenge is to find the sum of all multiples of 3 and 5 below 1000. Since we are looking for an answer before we begin our implementation, it's safe to spike using existing Ruby functionality.
(1..1000).select { |i| i % 3 == 0 || i % 5 == 0 }.reduce(:+)
=> 234168
After verifying that the answer is correct on the Project Euler site, we can write a test and start exploring the new features. Our test will provide a fall back as we make our changes.
describe "Ruby 2.0 features" do
context "Multiples of 3 and 5" do
it "Sum of all the multiples of 3 or 5 below 1000" do
FunctionalRuby.sum_multiples_of_3_and_5_for_range((1..1000)).should eq(234168)
end
end
end
This is arguably a bit verbose and of course the test fails, however, it's a good starting point so we can begin.
A Lazy Sequence to Start
The first Ruby 2.0 feature we will implement is Enumerator::Lazy but before we do, let's look back at our original spike.
(1..1000).select { |i| i % 3 == 0 || i % 5 == 0 }.reduce(:+)
=> 234168
At first glance this code seems harmless enough and many of us have probably implemented a similar solution. However, this code can be misleading because Ruby creates a complete array prior to executing the reduce(:+)
function. This proves particularly sticky when subsequent calls pull only a subset of data from the original array. The entire array must be built before retrieving the subset. Take a look at this example.
(1..Float::INFINITY).select { |x| x % 3 == 0 || x % 5 == 0}.take(5).reduce(:+)
Because the array must be completely built prior to calling take(5)
and it's an infinite set, the additional methods are never called.
A common idiom in functional programming is lazy evaluation. Lazy evaluation delays the functions execution until its value is needed. Ruby 2.0 introduces lazy evaluation through Enumerator::Lazy. This addition can solve our infinity problem by delaying the execution of the iteration.
(1..Float::INFINITY).lazy.select { |x| x % 3 == 0 || x % 5 == 0}.take(5).reduce(:+)
=> 33
So our next step is to implement a solution for our challenge using Enumerator::Lazy feature.
class FunctionalRuby
def self.sum_multiples_of_3_and_5_for_range(range)
range.lazy.select { |i| i % 3 == 0 || i % 5 == 0 }.reduce(:+)
end
end
When we run our tests, everything passes and all is well, kinda.
Refining with Refinements
Although we are moving in the right direction, our method doesn't really reflect a functional programming style. First, we have a single function with multiple responsibilities. Second, if we could curry the method calls it would provide a solution that is easier to read. Prior to Ruby 2.0 one might suggest monkey patching the Enumerator::Lazy module to add methods in order to chain them to lazy
. But we don't want our methods to pollute the Ruby module in every instance because these methods are specific to our solution. Ruby 2.0 offers a solution in the Refinements feature.
The Refinement feature is dependent on two methods: Module#refine
and using
. We can start our refactoring by adding a custom module and refining the Enumerator::Lazy module. Because it's unique to our Project Euler challenge we will name it accordingly.
module ProjectEuler
refine Enumerator::Lazy do
def multiple_of_3(number)
number % 3 == 0
end
def multiple_of_5(number)
number % 5 == 0
end
def select_multiples_of_3_and_5
select { |x| multiple_of_3(x) || multiple_of_5(x) }
end
def then_sum
reduce(:+)
end
end
end
class FunctionalRuby
using ProjectEuler
def self.sum_multiples_of_3_and_5_for_range(range)
range.lazy.select_multiples_of_3_and_5.then_sum
end
end
A quick run of the test to verify we are still passing and we can start to look at what's happening in the solution. The ProjectEuler
module has defined a refinement for the Enumerator::Lazy module and added the additional methods. Any class that applies the ProjectEuler
module through a using
method can take advantage of the monkey patch. This also includes overriding existing methods. We can DRY this code up by overriding the method_missing
call and consolidating the multiple_of_
methods.
module ProjectEuler
refine Enumerator::Lazy do
def method_missing(method, *args, &block)
if method.to_s =~ /multiple_of_(.*)$/
multiple_of($1.to_i, args[0].to_i)
end
end
def multiple_of(multiple, number)
number % multiple == 0
end
def select_multiples_of_3_and_5
select { |x| multiple_of_3(x) || multiple_of_5(x) }
end
def then_sum
reduce(:+)
end
end
end
Now this may be taking things to the extreme for this scenario but the purpose is to demonstrate that the method_missing
call can be overridden in this refinement without affecting the original module. We can test this by calling our custom method without applying the using statement to a class
class WithoutEuler
def self.call_multiple_of_3_on_lazy
(1..5).lazy.select_multiples_of_3_and_5
end
end
WithoutEuler.call_multiple_of_3_on_lazy
=> NoMethodError: undefined method `select_multiples_of_3_and_5' for #<Enumerator::Lazy: 1..1000>
Wrapping Up
It's clear that these examples are more academic then real-world but they provide an opportunity to review specific language features rather than focusing on the solution details. Ruby is a procedural language, but it can be fun to apply functional idioms to explore its nuances. These two features seemed like an excellent starting point for this type of experimentation. The Ruby 2.0 release has several more interesting features and we will be exploring many of them through similar experiments and reporting back our findings. Until then, happy hacking!