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