RubyでもErlangの[H|T]したいよ!
「プログラミングErlang」(Joe Armstrong著/榊原一矢訳)という本でちょっとErlangの世界を覗いているよ。
プログラミングErlang by Joe Armstrong
Erlangのような関数型言語はリスト処理に優れていて便利な構文がいろいろとあるんだね。例えばリストの先頭に別の要素を結合したものを |
を使って簡単に作れるんだ。
1> Langs = [haskell, erlang, lisp].
[haskell,erlang,lisp]
2> NewLangs = [ruby | Langs].
[ruby,haskell,erlang,lisp]
[ruby | Langs] のところがリストへの要素の追加になってるよ。ちなみにErlangでは変数は大文字で始まって、アトム1は引用符なしで書けるんだ。また式の終りには英語のようにピリオドを付けるよ。
一方で、リストから先頭要素を分離したものを作るには次のようにするよ。
3> [H|T] = Newlangs.
[ruby,haskell,erlang,lisp]
4> H.
ruby
5> T.
[haskell,erlang,lisp]
6>
変数Hにリストの先頭要素がバインドされて変数Tにリストの残りがバインドされる。
この何がうれしいかって言うと、これでリストの再帰的な処理がすごく簡単に書けるんだよ。試しにリストの要素を足し合わせるsum関数を定義してみるね。
sum([], N) -> N;
sum([H|T], N) -> sum(T, H+N).
sum([1,2,3,4,5], 0).
=> 15
リストが空のときは第2引数Nを返す。そうでないときは先頭要素HをNに足して残りのリストTでsum関数を再帰する。簡潔でカッコイイよねー。
リストの要素に関数を適用するmap関数も書いてみるよ。
map([], _) -> [];
map([H|T], F) -> [F(H)|map(T,F)].
map([1,2,3,4,5], fun(X)->X*X end).
=> [1,4,9,16,25]
リストが空のときは空リストを返す。そうでないときは先頭要素Hに関数Fを適用し、一方で残りのリストTでmap関数を再帰し、これらを結合してできるリストを返す。つまりここでは[H|T]を使ってリストの分離と結合をしてる。素敵だねー。ちなみにfun(X)->X*X end
はRubyのlambdaにそっくりだよね。
Rubyでやってみる
で、これを見てRubyでも |
でリストの結合や分離ができたらカッコイイと思ったんだ。
じゃあ少しやってみるね。Object#|
を定義するといろいろと問題がありそうなので、ここではFixnum, String,Symbolに対象を絞って実装するよ。
[String, Symbol].each do |klass|
klass.module_eval do
def |(other)
[self] + other
end
end
end
class Fixnum
alias :__OR__ :|
def |(other)
case other
when Array; [self] + other
else __OR__(other)
end
end
end
list = [2,3,4]
'a' | list # => ["a", 2, 3, 4]
:a | list # => [:a, 2, 3, 4]
1 | list # => [1, 2, 3, 4]
1 | 3 # => 3
これでリストの結合ができた。どうかな?
次にリストの分離だけれど、Array#|
を再定義して引数にIntegerが渡されたら先頭要素を分離するというのを考えたんだけど、なんかスマートじゃないんだ。
で、ここでハタと気が付いたんだけど、Rubyには既にカッコイイ分離方法があったんだよ。
list = [1, 2, 3, 4, 5]
a, *b = list
a # => 1
b # => [2, 3, 4, 5]
Rubyの多重代入はリストの分離に使えるんだ。
さて、Rubyでも再帰を使ってsumとmapを定義してみよう。Rubyはオブジェクト指向だからArrayのメソッドとしてこれらを定義するよ。まずは先の定義を使わない例を示すよ。
class Array
alias :head :first
def tail
drop 1
end
def sum(acc=0)
return acc if empty?
tail.sum(head+acc)
end
def mappy(&blk)
return [] if empty?
[blk[head]] + tail.mappy(&blk)
end
end
[*1..10].sum # => 55
[*1..5].mappy { |i| i * i } # => [1, 4, 9, 16, 25]
%w(ruby erlang haskell lisp).mappy { |n| n.capitalize } # => ["Ruby", "Erlang", "Haskell", "Lisp"]
ここではリストの先頭を返すheadメソッドと、残りを返すtailメソッドを別途定義しているよ。
次に、先に定義した |
と多重代入を使ったヴァージョンを示すよ。
class Array
def sum(acc=0)
return acc if empty?
head, *tail = self
tail.sum(head+acc)
end
def mappy(&blk)
return [] if empty?
head, *tail = self
blk[head] | tail.mappy(&blk)
end
end
[*1..10].sum # => 55
[*1..5].mappy { |i| i * i } # => [1, 4, 9, 16, 25]
%w(ruby erlang haskell lisp).mappy { |n| n.capitalize } # => ["Ruby", "Erlang", "Haskell", "Lisp"]
できたよ!
って、変数名にheadとtailを使ったからかなんか見た目に違いがなくて、あんまり面白くなかったね..
でもRubyの多重代入が、リストのheadとtailの分離に使えることに気づけたから僕自身は良しとするよ。
(追記:2010-8-16)RubyのArray#sumをErlangのsumに合わせて末尾再帰版に修正しました
- Rubyのシンボルのようなもの ↩
blog comments powered by Disqus