Reputation: 395
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
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
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
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
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
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 Resource
s:
def using_resources(*res, &block)
res = [*res]
# rest as above
end
Upvotes: 9