views:

205

answers:

3

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.

+1  A: 

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

-- MarkusQ

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

P.S. 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.

MarkusQ
I can figure out continuations myself if I have to, I was just really hoping for a more portable solution.
Justin Love
+1 - the Amb class looks like it already implements what you're looking for as a drop in solution. And I've seen no indication that callcc is being deprecated - http://ruby-doc.org/core-1.9/classes/Continuation.html
rampion
Hmm... I must have hit an outdated article. It does appear to be unsupported on some implementations, though probably not a deal breaker.
Justin Love
Kernel#callcc is getting rolled into a continuation library with a somewhat cleaner interface. But that does mean you'll have to think about portability.
MarkusQ
+1  A: 

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 (e.g. 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.

MarkusQ
I'll accept this because it answered the question I asked, but the real case is significantly more complicated. I'm a little intrigued by 'chain the choices together with yield'; could you point me to good article?
Justin Love
@JustinLove I can't find an article, but I'll try to post an example here in the next day or so.
MarkusQ
@Justine Love See below for example.
MarkusQ
+1  A: 

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"}
MarkusQ
Hmm... looks like lispy style iteration. Very cool, but in my case the choices aren't easily reducible to a data structure.
Justin Love