Ruby で内包表記

日本 Ruby 会議 2007 - Log0610-S5
RubyKaigi でみんな感動したと噂の Dave Thomas さんの講演のログ。やっぱり内容自体はどうでもよくて、ここが気になりました。

たとえば list comprehension がほしい。

Haskell の内包表記 (内包表現) とか確かにかっこいいけれど、Ruby の文法に Haskell の内包表記の文法を入れるのは明らかに無理だし、Python みたいな文法 ([x**2 for x in range(10)] みたいなの) は勘弁して欲しいですよね。それに、今の Ruby でも (ネタの範疇なら) 似たようなことができそう。そこで、以下のようなコードが動くようなものを作ってみました。

# [ x^2 | x <- [0..10] ] みたいなもの
p list{ x ** 2 }.where{ x.in(0..10) }
  #=> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# [ [x, y] | x <- [0..2], y <- [0..2], x <= y ] みたいなもの
p list{ [ x, y ] }.where{ x.in(0..2); y.in(0..2); x <= y }
  #=> [[0, 0], [0, 1], [0, 2], [1, 1], [1, 2], [2, 2]]

# sieve (x:xs) = x:sieve [ y | y <- xs, y `mod` x /= 0 ] みたいなもの
def sieve(x, *xs)
	ys = list{ y }.where{ y.in(xs); y % x != 0 }
	[x] + (ys.empty? ? [] : sieve(*ys))
end
p sieve(*(2..50))
  #=> [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

where より when のが適切? あと、やっぱり内包表記は無限リストがないとあんまり生きてこない気がします。
以下コード。
ポイントは以下の 2 つかな。

  • ブロックを define_method することで未定義変数の参照を method_missing で拾う
  • callcc で条件を満たすものを列挙する (まじめに考えてないのでバグあるかも)
class ListComprehension
   def initialize(&blk)
      plant(:value, blk)
   end
   attr_accessor :ctn

   # ブロックを真にするものだけリストアップする
   def where(&blk)
      plant(:cond, blk)
      ary = []
      @table = {}
      @state = :cond
      callcc do |ctn|
         @ctn = ctn
         if cond
            @state = :value
            ary << value
            @state = :cond
         end
         @ctn.call
      end
      ary
   end

   # ブロックをメソッドとして埋め込む
   # (未定義変数の参照を method_missing で拾うため)
   def plant(m, b)
      (class << self; self; end).class_eval { define_method(m, &b) }
   end
   def method_missing(m)
      r = (@table[m] ||= Slot.new(self))
      @state == :cond ? r : r.val
   end

   class Slot
      # 別のオブジェクトになるべくなりすますクラス
      instance_methods.each do |m|
         define_method(m) do |*a|
            @val.send(m, *a)
         end
      end
      def method_missing(*a, &b)
         @val.send(*a, &b)
      end

      def initialize(lc)
         @lc, @val = lc, nil
      end
      attr_reader :val # なりすますオブジェクト

      def in(ary)
         ctn = @lc.ctn
         ary.each do |e|
            callcc do |c|
               @lc.ctn = c
               @val = e
               return true
            end
         end
         ctn.call
      end

      def is(e)
         @val = e
      end
   end
end

def list(&b)
   ListComprehension.new(&b)
end