Refactoring Higher Order Messaging
In my
experiments with higher order messaging I implemented
convenient methods for selecting elements of a collection by
queries sent to those elements: "where",
"unless" and "having". These are equivalent to
various uses of the Enunmerable#select
method but, I
think, much more readable.
While working on the Ruby code that used higher order messages I
found I needed an equivalent to the Enumerable#any?
and Enumerable#all?
methods, which query if a
collection contains any or all elements that match a predicate.
Using higher order messages, I wanted to write statements like
those below.
if claimants.all_where.retired? then ... end if claimants.any_having.benefits > 0 then ... end
It was possible to write those higher order messages, of course, but only with a lot of duplicated logic. I got around this by splitting the higher order message objects into two: one object collects of results from individual elements, the other collates those results into a single total. The collector objects are higher order messages that define predicates over elements. They are common between all collators. The collators define whether those predicates are used to select a subset of the collection, or are combined by logical conjunction (all) or logical disjunction (any). Collectors are created by collators, like so
retired = claimants.select.where.retired? if claimants.any.having.benefits > 0 then ... end
The only problem now was that the select
method is
already defined by the Enumerable
mix-in. I needed a
new name. My solution was to change the naming convention of the
higher order messages and as a by product allow a more natural,
declarative expression of what was being calculated. In the new
style, the example above looks like:
retired = claimants.that.are.retired? if claimants.any.have.benefits > 0 then ... end
Very expressive, but perhaps a little too whimsical. Only time will tell.
With this refactoring, it's very easy to add new collators. The higher order messages are all defined in a base class and new collators can be created in only a few lines of code:
class Collator def initialize(receiver) @receiver = receiver end def are return HOM::Are.new(self) end def are_not return HOM::AreNot.new(self) end ... end class That < Collator def apply(&block) return @receiver.select(&block) end end class All < Collator def apply(&block) return @receiver.all?(&block) end end class Any < Collator def apply(&block) return @receiver.any?(&block) end end class Are < HigherOrderMessage def method_missing(id, *args) return @handler.apply {|e| e.__send__(id,*args)} end end class AreNot < HigherOrderMessage def method_missing(id, *args) return @handler.apply {|e| not e.__send__(id,*args)} end end
Well, that's enough higher order messaging. The code is available on RubyForge in the Homer project for anybody who wants to play with it.