Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Making Ruby Look Like <strike>Smalltalk</strike> <strike>Haskell</strike> <strike>Erlang</strike> Ruby

As Seen on TV

Inspired by Haskell

Did you ever want to write Ruby Code like:

x = 1
increment(x).by(6)

Now you can:

def increment(variable)
  chain do
    by do |delta|
      variable + delta
    end
  end
end

This is an OO version of a technique called Currying:

g = 'hello world'.index_of('o')
h = g.starting_at(6)

Inspired by Smalltalk:

'hello world' indexOf: $o startingAt: 6

Let’s do this in Ruby:

class String
  def index_of(substring)
    chain do
      starting_at do |starting_at|
        ...
      end
    end
  end
end

Now, in Ruby:

'hello world'.index_of('o').starting_at(6)

Inspired by Erlang

Here is pseudo-code for an interesting iteration pattern. If the Actor receives ‘lock’ it will not respond to any messages until it receives ‘unlock':

loop(X) ->
  receive
    'incr' -> loop(X+1)
    'lock' ->
      receive
        'unlock' ->
          loop(X);
      end
  end.

The Ruby equivalent:

def loop(x)
  puts x # added puts just to see what's going on
  chain do
    incr do
      loop(x+1)
    end
    lock do
      unlock do
        loop(x)
      end
    end
  end
end

Try this:

loop(1).incr.incr.incr => prints 1, 2, 3, then 4

Now, the finale: We can respond to incr any number of times till we’re locked; then, we respond to no messages other than unlock; once we’ve received unlock we proceed as before.

loop(1).incr.lock.incr => prints 1, 2, then raises an exception.
loop(1).incr.lock.unlock.incr => prints 1, 2, then 3

How does this work?

The call to chain do ... end creates a new Chain object with the block passed in to the constructor. Chain is kind of “blank slate”: all methods inherited from Object are undefined so that any messages it receives go through method missing. The block the Chain is instantiated with is instance-eval’d in the chain’s context, and all method invocations go through method missing (because of the blank slate). Method missing has two cases. It either dynamically defines a method returning a new link in the Chain (in the case of nested chaining), or it delegates the method back to the object that constructed the chain in the first place. Let’s consider examples of these two cases.

Case 1, dynamically defining a new method:

def foo
  chain do
    a do # define a method named :a on the Chain.
      1
    end
  end
end

foo.a => 1

Case 2, delegating the method back the the creator of the Chain:

def bar
  1
end

def foo
  chain do
    a do
      bar # invokes the bar defined above
    end
  end
end

foo.a => 1

Nested chaining is just a variation on Case 1:

def foo
  chain do
    a do
      b do # create a nested Chain (i.e., a Link)
        1
      end
    end
  end
end

foo.a.b => 1

The only gotcha is knowing whether a method invoked with a block belongs to the object that created the chain or is a nested chain:

def b(&block)
end

def foo
  chain do
    a do
      b do # is this the above b, or a nested Chain?
        ...
      end
    end
  end
end

We prioritize the #b defined on the parent object, rather than created a nested chain (I feel this is more intuitive).

Here is the source code:

require 'rubygems'
require 'active_support'

class Chain
  instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil?$|^send$|^instance_exec$)/ }
  delegate :define_method, :respond_to, :to => :__caller
  attr_accessor :__caller

  def __has_links?
    @__has_links
  end

  def initialize(*args, &block)
    if block_given?
      self.__caller = eval("self", block.binding)
      instance_exec *args, &block
    end
  end

  def method_missing(method, *args, &block)
    if block_given? && !__caller.respond_to?(method)
      @__has_links = true
      metaclass.module_eval do
        define_method method do |*args|
          __link(*args, &block)
        end
      end
    else
      __caller.send(method, *args, &block)
    end
  end

  private
  def __link(*args, &block)
    link = Chain.new
    link.__caller = __caller
    result = link.instance_exec(*args, &block)
    link.__has_links?? link : result
  end

  def metaclass
    class << self
      self
    end
  end
end

def chain(&block)
  Chain.new &block
end

Comments
  1. Shenpen says:

    I wonder whether I could do the same in Python with the new with-statement… I’ll try.

  2. Tordek says:

    x = 1
    increment(x).by(6)

    Huh?
    That “looks” cool, and may be “readable”, but OO-wise it makes no sense… “call increment (from nowhere?) passing the value X, and call “by” passing the value 6″?

    Why not a more OO style, like, say, x.increment(6) (you know, “noun.verb(object)”, normal and meaningful OO?)

  3. Chris Flipse says:

    Because it’s not OO. It’s pulled from a functional language.

  4. Chris Flipse says:

    Because it’s not OO. It’s pulled from a functional language.

  5. nick says:

    Tordek:

    x.increment_by(6) would be more idiomatic, OO, I agree; the example isn’t compelling in itself. But where it gets more complex: 'asdfa'.index_of('a').starting_at(2) we’re now firmly back in the OO realm, and this is arguably better than what might be more idiomatic Ruby: 'asdfa'.index_of('a', 0) or 'asdfa'.index_of(:char => 'a', :starting_at => 0)

  6. nick says:

    Also, compare this (awesome) feature of rspec:

    x = 0
    lambda {
      x = x + 1
    }.should change(x).from(0).to(1)
    
Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *