简体   繁体   中英

Execute remainder of ruby block more than once

I'm making a framework for specifying processes which may involve choices. I've got it working where each choice is an island. I'd prefer that subchoices 'fork' the parent choice, so that all options are properly considered.

choose :one => lambda {
    choose [a, b]
    if a
      raise "Illegal"
    end
  },
  :two => ....

Currently, it would always choose 'a' (which taken by itself looks better) but causes problems further down. Action :one with option 'b' is never considered.

I've run across callcc (not portable to all Ruby implementations, from what I've read) and fibers (new in 1.9 and can't be assumed to be available) as things that might be convinced to work, but I'm not crazy about having two implementations, or about the black magic of either of them, really.


I ended up taking the easy way out and passing the remainder of the computation as a block. This became a little less painful when I saw a similarity to an existing structure. I'm just hoping the indents don't get out of line.

The real case is significantly more complicated - there are side effects, but they are contained in a versioned key-value store. I'm also enumerating all possibilities and choosing the best one, so it can't just stop on success.

You may want to look through the solutions to [this quiz][1] for ideas.

-- MarkusQ

[1]: http://www.rubyquiz.com/quiz70.html "this quiz"

PS I'm on my way to a presentation but I'll check back and offer more when I get back, if no one else has stepped up to the plate.

Back, as promised.

Here are a few more ideas:

  • You could chain the choices together with yield to do an in order traversal of the permutations. In other words, choose could build a set of nested iterators out of the options passed to it, and they would just yield to the next one in the chain. Exiting the enclosed block puts you right back after the yield; if you need more (eg failure reasons) you could raise and rescue.
  • A funky arrangement of the three 'r's (rescue, raise, and retry) might do it, again with the idea that choose is nesting the option bodies or embedding them in a nested structure.
  • If the options are cheap and side effect free, you might want to look at just producing all the permutations and iterating through them.
  • If they aren't side effect free, you may want to try some sort of pseudo-monad solution, where you lazily produce lambdas for each permutation.
  • More or less equivalent (but straying ever farther from your initial question) you might be able to assign them an index (easiest if you could determine the cardinality of each choice, but possible with a segmented index in any case) and iterate through the indexes.
  • Fibers have been backported to 1.8.x

But all things considered, I think your best answer would be to wrap the functionality you want in a class or function, implement it with callcc , and then do version detection in or around the definition of this as needed so that the right implementation is used in the right version of ruby.

As requested, here's an example of what I meant by chaining the choices together with yields. A bare bones implementation might look something like this:

def choose_one_of_each(choices,results,&block)
    if choices.empty?
        yield results
      else
        c = choices.dup
        var,val = c.shift
        choose(val) { |v|
            choose_one_of_each(c,results.update(var => v),&block)
            }
      end
    end

def choose(options,&block)
    case options
      when Hash  then choose_one_of_each options,{},&block
      when Range then options.each { |item| yield item rescue nil }
      else            options.each { |item| yield item rescue nil }
      end
    end

And you'd use it like this (somewhat expanded from your example, to show how the parts interact):

a = 7
b = 'frog'
choose(
    :one => [a,b], 
    :two => ['stay','go','punt'], 
    :three => {:how => ['in the car','in a boat','by magic'],:how_fast => 0..2 }
  ) do |choices|
     raise "illegal" if choices[:one] == a
     raise "You can't stay fast!" if choices[:two] == 'stay' and choices[:three][:how_fast] > 0
     raise "You go that slow!"    if choices[:two] == 'go'   and choices[:three][:how_fast] < 1
     print choices.inspect,"\n"
     end

Which would produce something like this (because of the print):

{:three=>{:how=>"in the car", :how_fast=>0}, :one=>"frog", :two=>"stay"}
{:three=>{:how=>"in the car", :how_fast=>0}, :one=>"frog", :two=>"punt"}
{:three=>{:how=>"in the car", :how_fast=>1}, :one=>"frog", :two=>"go"}
{:three=>{:how=>"in the car", :how_fast=>1}, :one=>"frog", :two=>"punt"}
{:three=>{:how=>"in the car", :how_fast=>2}, :one=>"frog", :two=>"go"}
{:three=>{:how=>"in the car", :how_fast=>2}, :one=>"frog", :two=>"punt"}
{:three=>{:how=>"in a boat", :how_fast=>0}, :one=>"frog", :two=>"stay"}
{:three=>{:how=>"in a boat", :how_fast=>0}, :one=>"frog", :two=>"punt"}
{:three=>{:how=>"in a boat", :how_fast=>1}, :one=>"frog", :two=>"go"}
{:three=>{:how=>"in a boat", :how_fast=>1}, :one=>"frog", :two=>"punt"}
{:three=>{:how=>"in a boat", :how_fast=>2}, :one=>"frog", :two=>"go"}
{:three=>{:how=>"in a boat", :how_fast=>2}, :one=>"frog", :two=>"punt"}
{:three=>{:how=>"by magic", :how_fast=>0}, :one=>"frog", :two=>"stay"}
{:three=>{:how=>"by magic", :how_fast=>0}, :one=>"frog", :two=>"punt"}
{:three=>{:how=>"by magic", :how_fast=>1}, :one=>"frog", :two=>"go"}
{:three=>{:how=>"by magic", :how_fast=>1}, :one=>"frog", :two=>"punt"}
{:three=>{:how=>"by magic", :how_fast=>2}, :one=>"frog", :two=>"go"}
{:three=>{:how=>"by magic", :how_fast=>2}, :one=>"frog", :two=>"punt"}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM