継続による非決定計算の実装

SICP を読んでいる今日この頃だが、Ruby でも継続が使えると知って、実際にやってみた。処理系は Windows の ActiveScriptRuby 1.8.7。最後に触ったの、いつだっけ……。そもそも今回のプログラムを書くために Ruby の文法を1日かけて復習しなければならなかった程度の知識なので、おかしいところがあるかもしれないというか、あって当然というか。しかし 1.9 でも継続は実装されているらしいと聞いたので、これはガゼン勉強せねばなるまいな、と思うのであった。

class Backtrack
  def initialize(endval = nil)
    @endval = endval
  end

  def format
    @paths = []
    callcc do |cc|
      @back_proc = lambda do
        if @paths.empty?
          cc.call @endval
        else
          @paths.pop.call
        end
      end
    end
  end

  def back
    @back_proc.call
  end

  def assert(val)
    back unless val
  end

  def choose(arr)
    callcc do |ret|
      arr.each do |choice|
        callcc do |cc|
          @paths.push cc
          ret.call choice
        end
      end
      back
    end
  end
end


この Backtrack クラスは、インスタンス内部に継続のスタックを保持する。メソッド choose は enumerable なオブジェクトを受け取り、そこから先頭要素1つを返すとともに、残りを辿るための継続をスタックにプッシュする。メソッド back が呼ばれるとスタックから継続を取り出して、それを call することでバックトラックを行う。


使用方法:
インスタンスを作る。コンストラクタ引数は、辿る継続がなくなったときに返される値。
次にバックトラックの起点にしたい場所でメソッド format を呼ぶ。たいていはトップレベルだろう。

bt = Backtrack.new :done

bt.format

Ruby における継続というのは、ローカル変数とトップレベルまでの道筋を保存しておくオブジェクトのようだ。かつて SECD マシンという種類の仮想マシンScheme を実装したときには、その時点でのコールスタックと変数環境とコードの残りを保存したクロージャ、という感じだった。
さて今回の場合、バックトラックは最終的に format した場所に戻ってくる。ブロックの内側とかに起点を作ると、あとで back を呼んだとき過去にタイムリープするという愉快なことになる。例えば以下のコードを実行したとすると:

x = 0

begin
  bt.format
  if 4 == x
    'return'
  else
    x += 1
    puts 'go back'
    bt.back
  end
end

"go back" と4つ表示されたのちに 'return' が返ってきて終了するが、そのあとさらに以下のようなことをすると:

x = 2

bt.back

処理が bt.format に戻り、"go back" と2度表示されたあと 'return' が返ってくる。
さて、非決定計算をするにはメソッド choose を使う:

bt = Backtrack.new :done
bt.format

[bt.choose( [1,2] ), bt.choose( [3,4] )]
#=> [1, 3]

choose は each を使ってコンテナを辿るので(ここでも継続を使っている)、引数は配列でなくてもかまわない。
バックトラックして choose に次の選択肢を選ばせるには、back を使う:

bt.back  #=> [1, 4]

辿れる選択肢がなくなったあとは(つまり @paths が空になったら)、コンストラクタに与えた引数が返ってくる:

bt.back  #=> [2, 3]
bt.back  #=> [2, 4]
bt.back  #=> :done

メソッド assert は1つの値を受け取り、それが偽のときだけ back を呼ぶ。これを使って SICP の論理パズルを解いてみよう(文章はシンプルにしてある)。


5階建てのアパートに A, B, C, D, E の5人が住んでいる。部屋のある階はみな互いに異なる。
Aの部屋は5階ではない。
Bの部屋は1階ではない。
Cの部屋は1階でも5階でもない。
Dの部屋はBよりも上の階にある。
Eの部屋はCの隣の階ではない。
Cの部屋はBの隣の階ではない。

def multiple_dwelling(bt)
  a = bt.choose [1, 2, 3, 4, 5]
  b = bt.choose [1, 2, 3, 4, 5]
  c = bt.choose [1, 2, 3, 4, 5]
  d = bt.choose [1, 2, 3, 4, 5]
  e = bt.choose [1, 2, 3, 4, 5]
  bt.assert( ! [a, b, c, d, e].uniq! )
  bt.assert( a != 5 )
  bt.assert( b != 1 )
  bt.assert( c != 1 && c != 5)
  bt.assert( d > b)
  bt.assert( 1 != (e - c).abs )
  bt.assert( 1 != (c - b).abs )
  [a, b, c, d, e]
end

この関数に先程の bt を渡せば、答えの配列が返ってくる。さらに back しても :done が返ってくるだけなので、答えがただ1つであると分かる。