PythonやHaskellやErlangにはリスト内包表記と呼ばれる、リストの中で新たなリストを生成する構文があるよ。例えばRubyでリストの要素の値を倍にしたい場合はArray#mapを使うよね。

l = [*1..10]
 l.map { |i| i*2 } # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

これをErlangのリスト内包表記では以下のように書けるんだ

L = lists:seq(1,10).
 [X*2 || X <- L].  % => [2,4,6,8,10,12,14,16,18,20]

リストLからXを選び出しそれに2を掛けたものを返す。つまり || の左辺には出力となる式を、右辺には限定子を書く。X <- LはErlangではGeneratorと呼ぶらしいよ。

次にリストから偶数だけを選んで、それらを倍にしたい場合を考えるよ。Rubyなら次のように書くよね。

l.select(&:even?).map { |i| i*2 } # => [4, 8, 12, 16, 20]

またはこう書くよ。

l.map { |i| i*2 if i.even? }.compact # => [4, 8, 12, 16, 20]

これがErlangではこう書けるんだよ。

[X*2 || X <- L, X rem 2 =:= 0].  % => [4,8,12,16,20]

リストの最後の項がfilterになって、選び出されるXを限定する。Rubyもいい線いってるけど、リスト内包のほうが宣言的でわかりやすいかな。

さらに、リストから偶数かつ5より大きい数だけを選んで倍にする場合をやってみるよ。まずはRuby。

l.select { |i| i.even? && i > 5 }.map { |i| i*2 } # => [12, 16, 20]

または

l.map { |i| i*2 if i.even? && i > 5 }.compact # => [12, 16, 20]

Erlangだと次のようになるよ。

[X*2 || X <- L, X rem 2 =:= 0, X > 5]. # => [12,16,20]

複数のfilterをカンマ区切りで指定できる。簡潔だよね。

さらにもう一歩進んでみよう。3つの異なる範囲のリスト(l1=1~5, l2=3~7, l3=5~9)があって、それらから一つずつ選択された数の合計が11になるものを求めるよ。前の記事で紹介したように、RubyではArray#productを使えば簡単にできるよね。

l1 = [1,2,3,4,5]
l2 = [3,4,5,6,7]
l3 = [5,6,7,8,9]
l1.product(l2, l3).select { |a,b,c| a + b + c == 11  }
# => [[1, 3, 7], [1, 4, 6], [1, 5, 5], [2, 3, 6], [2, 4, 5], [3, 3, 5]]

これをErlangのリスト内包表記では次のように書けるんだ。

L1 = [1,2,3,4,5].
 L2 = [3,4,5,6,7].
 L3 = [5,6,7,8,9].
 [{A,B,C} || A <- L1, B <- L2, C <- L3, A + B + C =:= 11].
 % => [{1,3,7},{1,4,6},{1,5,5},{2,3,6},{2,4,5},{3,3,5}]

わかりやすいね。つまりリスト内包では複数のgeneratorを指定できて、それらから要素が良しなに取り出されて、filterの条件にマッチする組だけが生成される。

Rubyのproductも簡潔ではあるけれども、あらかじめすべての組み合わせが生成されてしまう、という点がイマイチかな。

Rubyで実装を試みる

そんなわけで、Rubyでなんとかリスト内包表記っぽいことができないか考えてみたよ(ネタとして)。

最初に考えた構文は次のとおりだよ。

class Array
  def %(ary)
    map(&ary[0]).compact
  end
end
list = [*1..10]
list % [->x{x*2 if x.even?}] # => [4, 8, 12, 16, 20]

Array#%を定義してその引数としてProcオブジェクトを一つ含む配列を取る。そして渡すProcの中でgeneratorとfilterを指定するよ。

ary[0]とするのがダサいよね。

ということで、次のようなものも考えてみたよ。

list = [*1..10]
list. <=[->x{x*2 if x.even?}] # => [4, 8, 12, 16, 20]

一瞬でこの実装がわかる人はいる?ちょっと凝ってみたんだけど..

実装は次のとおりだよ。

class Array
  def <=
    ->x { map { |e| x[e] }.compact }
  end
end

つまりArray#<=を引数なしで呼んで、それが返したProcオブジェクトをProc#[]でcallしてる。->x{x*2 if x.even?}はその引数となるProcオブジェクトだよ。<=[]とするとProcの呼び出しに全く見えないよね。

でもまだ->x{x*2 if x.even?}がイケテない。せめてgeneratorとfilterに分けたい。それで次のようにしてみたよ。

class Array
  def <=
    ->gen,*preds {
      select { |e| preds.all? { |pred| pred[e] } }
      .map { |e| gen[e] }
    }
  end
end
list = [*1..10]
list. <=[->x{x*2}, ->x{x.even?}, ->x{x>5}] # => [12, 16, 20]

こうすればfilterをカンマ区切りでいくつでも追加できる。

でも正直->x{ }がいくつも連続するのはヒドすぎるねー。

list. <=[x*2, x.even?, x>5] # => [12, 16, 20]

のように出来ればいいんだけど。xは未定義だから構文エラーになっちゃう。それに複数のgeneratorを渡すこともできないから、先の3つのリストの合計を取るような問題にも対応できない..

RBridge

で諦めかけたそのとき..

全く別のアプローチに気が付いたんだよ!

次のコードは先の3つのリストの合計を取る例を関数sum_toとして実装してるんだ。

def sum_to(n, a, b, c, x=<<-ERL)
  [ {A, B, C} ||
      A <- #{a},
      B <- #{b},
      C <- #{c},
      A + B + C =:= #{n}
  ]
  ERL
  x.evarl
end
a = [1,2,3,4,5]
b = [3,4,5,6,7]
c = [5,6,7,8,9]
sum_to(11, a, b, c)
# => [[1, 3, 7], [1, 4, 6], [1, 5, 5], [2, 3, 6], [2, 4, 5], [3, 3, 5]]

関数sum_toの中身はErlangのコードそのままだよ。ヒアドキュメントによりErlangのコードを文字列化し、これにevarlメソッドを送ってる。

もう気が付いた人もいると思うけど..

そう、裏でErlangサーバーを起動してRubyから呼んでいるのでした!String#evarlの実装は次のとおりだよ。

require "rbridge"
class String
  def evarl
    @@erl ||= RBridge.new(nil, "localhost", 9900)
    @@erl.erl self
  end
end

RBridgeというRubyからErlangサーバに接続できる拡張ライブラリが実はあるんだよ!

gem install rbridgeでインストールして、シェルでrulangコマンドを実行してサーバを起動する。デフォルトの待ち受けポートは9900になるよ1

そして先のコードのように指定ポートでRBridgeのインスタンスを生成し、RBridge#erlにErlangのコードを含む文字列を渡す。するとbeamというErlangのエミュレータでこれを解析し、結果をRubyの形式で返すよ。

便利なものを作ってくれる人が世の中にはいるもんだね!

id:ku-ma-meさんによるRubyの内包表記

で、世の中には他にもすごい人がいるんだよ2。Rubyで実用レベルのリスト内包表記類似の構文ができてるんだ。

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]

すごいよね。で、この構文を見て実装がどうなっているか、想像できる人はどれくらいいるのかな。もう僕にはまったく歯が立たなかったよ。変数x yは一体..

そしてその実装を見ても..

まだまだ僕は精進が必要だよ。

ちなみにRuby1.9だとcontinuationをrequireする必要があるよ。

参考サイト:

Rulang BridgeでRubyからErlangを呼び出してみた - うなの日記(現在の実装はこの記述とは少し異なっています)

  1. サーバの停止はps aux \| grep rulangなどとしてPIDを見つけてkill PIDしてください
  2. 改めて言うまでもありませんが..


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS