/ ruby

Rock Your Ruby: The Value of Value

Check out the Ruby Gem at GitHub

Introduction

Go ahead and ask the developer sitting next to you what is the thing that loves
the most about Ruby. It should come as no surprise that simplicity, flexibility
and expressiveness are the main reasons Ruby junkies just can't get enough of
it.

In Matz own words, seems like the 'go with the flow' philosophy was present
right from the start in the Ruby community:

{% blockquote %}
"Actually, I'm trying to make Ruby natural, not simple."
{% endblockquote %}

Cuttin' loose from the language cliché

I do believe in the principles and values Ruby was built upon, but I don't like
clichés. We should be critic about the restrictions a programming language
impose on us, and really pay attention when things don't feel right.

Here's a concrete example to show you what I'm talking about. Check out these
uses of the :detect message:


discount_rules.detect(lambda{ default_discount_rule }) { |rule| rule.applies_on? a_product }
students.detect(lambda{ anonymous_student }) { |student| student.is_named? 'John' }

Do they 'feel natural' to you? Do you think there is something wrong with them?
Take another look.

Noise & Duplication

There are two things that fire my alarms about these examples.

First, the unnecessary noisy syntax[1] needed to pass the block that gets
evaluated when no object matched the :detect condition[2]. I feel that
departs from the original intent of Ruby (to feel natural to the developer) by
forcing the use of 'tech' terms.

Second, the presence of repeated code. I don't mind writing blocks if I really
need to, but they seem overkill when all I want is to return an object to handle
the "if none" case.

Dealing with duplicated code

In the Object Oriented paradigm repeated code is a symptom of a missing
abstraction, a concept that is not being modeled. As a consequence, what would
be its implementation is scattered around the methods that were supposed to use
it.

It should be clear from the examples that repeated code does not mean repeated
text, but repeated collaborations of message sends. These examples were choosen
to demonstrate that repeating code involves repeating collaborations of message
sends, instead of simply repeating text[3].

Let's take a look at what is repeated, and what is not, between the two uses of
:detect:


discount_rules.detect(lambda{ default_discount_rule }) { |rule| rule.applies_on? a_product }
students.detect(lambda{ anonymous_student }) { |student| student.is_named? 'John' }

# the collaboration pattern seems to be:
FOO.detect(lambda{ BAR }) { BAZ }

Whenever we want to return an object when no object matches a condition we're
wrapping it with a lambda, because that is what the :detect message expects
as first collaborator.

One way to go would be to add a new method to the Enumerable module that does
the dirty lambda wrapping for us, but that solution would only work for
:detect.

It would be great to come up with a more generic solution, one that works for
any method that expects a block to be passed in.

Designing a generic solution

We would like is to be able to express these examples in a more natural way,
getting rid of the extra lambda parts:


# look ma' no lambdas!
discount_rules.detect(default_discount_rule) { |rule| rule.applies_on? a_product }
students.detect(anonymous_student) { |student| student.is_named? 'John' }

Let's start solving this "challenge" not only by writing a test first, but
writing the test's assert first:


it 'should be possible to pass any object when a block without arguments is expected' do
  apple = Object.new
  fruits = []
    
  fruit = fruits.detect(apple) { |fruit| false }
  
  fruit.should == apple
end

After running it (and watching it turn red) the failure message reveales the
root of the problem:

NoMethodError: undefined method `call' for #<Object:0x00000001390d68>

Besides the missing message implementation, what this really means is that
instances of Object are not polymorphic with lambdas (instances of Proc)
with respect to the :call message.

To make this tests pass, let's do the simplest thing possible and add the
:call method returning self to Object.

But hey, we can do better! What about being able to pass an arbitraty object to
the :select method (which expects an implicit block that takes one external
collaborator)?


it 'should be possible to pass any object when an implicit block with arguments is expected' do
  apple = Object.new
  orange = Object.new
  fruits = [apple, orangee]
  apples_only = FruitFilter.new apple

  selected_fruits = fruits.select &apples_only

  selected_fruits.should have(1).item
  selected_fruits.should include apple
end

FruitFilter[4] is a test class I used to express better the intent of the
test.

This time, the failure message is a little more cryptic:

TypeError: wrong argument type Object (expected Proc)

We need an instance of Proc to be passed in to :select. The unary &
operator converts blocks to procs, but Object is not a proc and neither
knows how to respond to the :to_proc message that gets sent when using &.

Let's do the simplest thing to pass the test, and implement :to_proc in
Object, returning a proc that evaluates self.call (that was implemented in
the first test).

Here's how Object looks like after passing the tests:


class Object

  def call *args
    self
  end

  def to_proc
    proc{ |*args| self.call *args }
  end

end

There are still some test cases left to be consider to make sure everything
works as expected/nothing was broken (like backwards compatibility with Proc,
Method and Symbol :call and :to_proc methods)[5].

What's the deal with the "Value of Value"?

The need to reduce the friction when working with blocks was motivated by the
way Smalltalk solves the problem. Any object knows how to respond to the
#value message (which behaves in the same as the :call message just
implemented).

Even though the implementation is pretty straight forward, just returning
self, relying on #value in Smalltalk allows us to express domain concepts
better, making the code easier to read by dealing with objects or blocks in the
same way[6].

From my point of view, it is a little method that adds great value.

Conclusion

By working the solution step by step through TDD, we're now in a position to
explain and tell the cause of repeated code in the initial examples: instances
of Object were not polymorphic with instances of Proc. That forced us to
wrap objects in lambdas so :detect could work as expected.

Two missing abstractions (methods, in this case) were implemented in Object to
get rid of repeated code, :call and :to_proc methods.

Besides the immediate benefit of the implemented feature, I really enjoyed the
oportunity to strictly follow the OO paradigm and TDD to see how far they can be
taken. I valued the fact that Ruby can be modified to make it fit my needs.

Do not take for granted a language is expressive or feels natural just because
that's what the documentations says and the community accepts it without
questioning. Learn from the past (as we did from Smalltalk in this case) to
avoid reinventing the flat tire.

Don't get trapped in the language cliché. Build your own Ruby.


  1. I'm talking about the lambda/proc keywords preceding the braces needed to create a Proc. ↩︎

  2. see :detect method documentation at RubyDoc ↩︎

  3. If you want to understand or learn more about this concept, enroll in one of our courses! See Diseño Avanzado de Software Con Objetos I and Diseño Avanzado de Software Con Objetos II ↩︎

  4. FruitFilter source code ↩︎

  5. Check the whole test suite at GitHub ↩︎

  6. Dont' just stand there! Go ahead, grab a copy of Pharo (a Smalltalk dialect) and browse for implementors and senders of #value ↩︎