Chaining

marick edited this page Jan 9, 2012 · 4 revisions

Motivation

In some thoughts on classes after 18 months of Clojure, I suggested that functional code in Ruby would still benefit from OO-style "dot notation" for applying functions. That is, rather than write this:

   new reservation = save!.(without_animals_in_use.(with_changed_timeslice.(reservation, timeslice)

... we should write this:

   new_reservation = 
      reservation.
      with_changed_timeslice(timeslice).
      without_animals_in_use.
      as_saved

This can often be done by defining our "functions" as methods and invoking them in the normal Ruby way. But methods aren't functions, not really. What if we want to use, say, a lambda defined on the fly by another lambda---a typical use of higher-order functions?

Therefore, Stunted provides a way to insert true lambdas into such a chain. Before doing so, we have to think about what to do about self. In a message chain like the above, each method is implicitly handed, via self, the result of the previous method. In that specific example, each method creates a new "reservation", so that sequence of calls looks very like this Clojure code:

    (-> reservation
        (with-changed-timeslice timeslice)  ; call to (fn [reservation timeslice] ...)
        without-animals-in-use              ; call to (fn [reservation] ...) 
        as-saved)                           ; call to (fn [reservation] ...)

If we stick an explicit lambda in the method chain, we can handle self in two ways:

  • pass it as an explicit argument

  • do instance_exec magic to make it available through the pseudo-variable self.

And why not do both! (Note: the following examples are included with the source.)

Chaining with an explicit self argument

Suppose we have a boring class that just holds a value. It's immutable but has a method to create a new instance with an incremented value:

class ValueHolder
  attr_reader :value

  def initialize(value)
    @value = value
  end

  def bumped
    self.class.new(value + 1)
  end
end

It chains nicely:

puts ValueHolder.new(0).bumped.bumped.value   #=> 2

Let's add to it the ability to pass itself to arbitrary lambdas:

class ValueHolder
  include Stunted::Chainable
end

puts ValueHolder.new(0).
     bumped.
     pass_to(-> _ { _.value + 1000 }).     # I'm using _ to represent "self"
     succ     #=> 1002

The parentheses around the lambda are required because of Ruby's syntax. One way to avoid them is by passing a block (which pass_to converts to a real lambda):

puts ValueHolder.new(0).
     bumped.
     pass_to { | _ | _.value + 500 }.
     succ     # 502

Here's an example of using higher-order functions:

make_adder =
  lambda do | addend |
    -> _ { _.value + addend }
  end

puts ValueHolder.new(0).
     bumped.
     pass_to(make_adder.(33)).
     succ     # 35

Chaining with an implicit self argument

defsend is a essentially a wrapper around instance_exec. The name is supposed to suggest that you're quickly defing an anonymous method, sending its "name" to the object containing it, and then throwing it away. Using the same ValueHolder class and inclusion of Stunted::Chainable, you can write code like this:

puts ValueHolder.new(0).
     bumped.
     defsend(-> { self.class.new(@value + 1000) }).
     defsend { self.class.new(value +   330) }.
     value     # 1331

Here's a different use of a higher-order function:

make_adder =
  lambda do | addend |
    -> { self.class.new(@value + addend) }
  end

puts ValueHolder.new(0).
     bumped.
     defsend(make_adder.(33)).
     value     # 34

Passing more than just the self argument

In both their block and lambda forms, pass_to and defsend can pass along extra arguments.

Here's the lambda form:

puts ValueHolder.new(0).
     pass_to(-> _, addend { _.class.new(_.value + addend) }, INCREMENT_VALUE).
     defsend(-> addend { self.class.new(@value + addend) }, INCREMENT_VALUE).
     value  #=> 400

The block form looks awkward but works:

puts ValueHolder.new(0).
     pass_to(INCREMENT_VALUE) { | _, addend | _.class.new(_.value + addend) }.
     defsend(INCREMENT_VALUE) { | addend | self.class.new(value + addend) }.
     value   #=> 400