bvk
bvk

Reputation: 395

dynamic nesting of ruby blocks

Imagine i have some resource objects with a run method, which executes the block parameter under the lock held for that resource. For example, like this:

r = Resource("/tmp/foo")
r.run { ... }

How can I write a ruby method which takes an array of resources and executes its block parameter under the lock held for all resources, like:

def using_resources(*res, &block)
  r[0].run do; r[1].run do; r[2].run do ...   

    yield;

  end; end; end; ...
end

Is it possible at all?

Upvotes: 6

Views: 1705

Answers (3)

ConvexMartian
ConvexMartian

Reputation: 331

I'd like to post an extension to @rampion's solution for the case where run() yields a value that you would like to use in the innermost block, like this:

def using_resources(*res, &block)
  r[0].run do |v0|; r[1].run do |v1|; r[2].run do |v2| ...

    yield [v0, v1, v2];

  end; end; end; ...
end

For example, this would arise if run was like File.open, where it yields a resource (eg, a file object) that you can use in your given block, but which is torn down after your block completes.

Here is the function:

def nested_do( args, func, &block )
  args.reverse.inject(block) do |inner, a|
      Proc.new do |acc|
          func.call(a) do |v|
              acc.append(v)
              inner.call(acc)
          end
      end
  end
  .call([])
end

To nest calls to run and collect up their yielded values, you would do:

res = [ Resource('a'), Resource('b'), Resource('c') ]
func = Proc.new {|r,&b| r.run(&b)}
nested_do( res, func ) do |vals|
    puts("Computing with yielded vals: #{vals}")
end

Example

Here is a function like File.open, except it opens a "secret" resource, which the caller can never use in the caller's scope, because it is torn down after the yield() returns:

def open_secret(k)
    v = "/tmp/secret/#{k}"
    puts("Setup for key #{k}")
    yield v
    puts("Teardown for key #{k}")
end

Here is how to dynamically nest calls to open_secret:

nested_do([:k1,:k2,:k3], Proc.new {|k,&b| open_secret(k,&b)} ) do |rs|
    puts("Computing with 'secret' resources SIMULTANEOUSLY: #{rs}")
end

Prints:

Setup for key k1
Setup for key k2
Setup for key k3
Computing with 'secret' resources SIMULTANEOUSLY: ["/tmp/secret/k1", "/tmp/secret/k2", "/tmp/secret/k3"]
Teardown for key k3
Teardown for key k2
Teardown for key k1

Update

In hindsight, using inject required a lot of brain-bending. Here is a superior implementation that uses recursion:

def nested_do(args, func, acc=[], &block)
    return block.call(acc) if args.size == 0

    a = args.pop
    func.call(a) do |v|
        acc.append(v)
        nested_do( args, func, acc, &block)
    end
end

Upvotes: 1

rampion
rampion

Reputation: 89053

You could also do this using #inject:

def using_resources(*resources, &block)
  (resources.inject(block){ |inner,resource| proc { resource.run(&inner) } })[]
end

As you step through the array, you wrap each resource's invocation of the previous Proc in a new Proc, and then pass that to the next resource. This gets the locks in reverse order (the last resource given is the first one unlocked), but that could changed by using resources.reverse.inject ...

Upvotes: 10

Senad Uka
Senad Uka

Reputation: 1131

It seems to me that this is best done using recursion

Here is the code:

def using_resources(*res, &block)

   first_resource = res.shift

   if res.length > 0 
     first_resource.run do
      using_resources(*res, &block)
     end
   else
     first_resource.run do
       block.call
     end
   end

end

And use it like so:

using_resources Resource.new('/tmp'), Resource.new('http://stackoverflow.com') do
  do_some_processing
end

You do say, "which takes an array of resources." If you have an Array already and need to use it, you can either splat the Array outside the call:

 using_resources *my_array do
   do_some_processing
 end

Or inside the method definition, which allows you to call it either with an Array or a list of Resources:

def using_resources(*res, &block)
  res = [*res]
  # rest as above
end

Upvotes: 9

Related Questions