Making Ruby Look Like Smalltalk Haskell Erlang Ruby

December 9, 2007 Pivotal Labs

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

About the Author

Biography

More Content by Pivotal Labs
Previous
Pivots at RubyConf
Pivots at RubyConf

The Confreaks site has released a slew of videos from the 2007 RubyConf. The videos are in the perfect form...

Next
Ruby Quiz (A Trick Question)
Ruby Quiz (A Trick Question)

Here is a little Ruby trivium for you. Type this into IRB: def foo def bar 1 end end foo.bar => ...

How do you measure digital transformation?

Take the Benchmark