Rubyでもリスト内包表記したい?
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で実用レベルのリスト内包表記
類似の構文ができてるんだ
# [ 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を呼び出してみた - うなの日記(現在の実装はこの記述とは少し異なっています)