I recently ran into an fairly uncommon situation. I needed to nest multiple method calls around a single block:
Model1.transaction do
Model2.transaction do
Model3.transaction do
all_models.each{|o| o.do_something! }
end
end
end
The uncommon part was that the objects I was calling the methods on could potentially change. So I had an array containing all of them: model_classes = [ Model1, Model2, Model3 ]. Now the dilemma is given the array how do you produce the above effect, dynamically. I came up with this:
do_something = lambda { all_models.each {|o| o.do_something! } }
model_classes.reverse.inject(do_something) do |l, obj|
lambda { obj.transaction { l.call } }
end.call
Here’s a clearer way to see how the above builds up the chain of lambdas in reverse:
lambda_do_something = lambda { all_models.each {|o| o.do_something! } }
lambda_model3 = lambda { Model3.transaction { lambda_do_something.call } }
lambda_model2 = lambda { Model2.transaction { lambda_model3.call } }
lambda_model1 = lambda { Model1.transaction { lambda_model2.call } }
lambda_model1.call
Now abstract it away and we get nest_and_yield!!
class NestedYielder
instance_methods.each { |m| undef_method m unless m =~ /^__/ } # start fresh
def initialize(enum); @enum = enum; end
def method_missing(m, *args, &blk)
@enum.reverse.inject(blk) do |l, obj|
lambda { obj.send(m, *args) { l.call } }
end.call
end
end
module Enumerable
def nest_and_yield
NestedYielder.new(self)
end
end
Here’s some examples of how to use it:
model_classes.nest_and_yield.transaction { ... }
model_classes.nest_and_yield.with_scope(:some_conditions => :wahtever) { ... }
# => Model1.with_scope(:some_conditions => :whatever) { Model2.with_scope(:some_conditions => :whatever) { ... } }
mutexes.nest_and_yield.synchronize { ... }
# => mutex1.synchronize { mutex2.synchronize { ... } }
It really needs a better name, any suggestions?