Heading image for post: Functional Eye for the Ruby Guy

Ruby

Functional Eye for the Ruby Guy

Profile picture of Johnny Winn

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!

More posts about Ruby Development

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project