Ruby と関数型プログラミングについてのメモ(1)

jp.quora.com
まつもとゆきひろさんは、Ruby を「関数型言語と呼ぶのにはだいぶ抵抗があります」といっておられる。これでもう結論は出たようなものだが、それでも、Ruby にはだいぶ関数型言語の考え方が取り入れられているのも事実だ。それをちょっとだけ考えてみる。
 

Array, Enumerable, Enumerator

配列a = [1, 2, 3]の各要素を 2倍した新しい配列new_aが欲しい。これを「手続き型」プログラミングを使ってやるとどうなるか。

a = [1, 2, 3]
new_a = []
for i in a
  new_a << i * 2
end
new_a    #=>[2, 4, 6]

例えば、こんな感じ?

でも、たぶん Ruby初心者でも、あんまりこんな風には書かないよね。Array#mapを使って、こう書くと思う。

a = [1, 2, 3]
new_a = a.map { |i| i * 2 }    #=>[2, 4, 6]

このmapが既に関数型プログラミングの考え方を使っている。この場合、ブロックが「関数」の役割を果たしている。そもそも Ruby でブロックを使うこと自体、関数型プログラミングっぽいのだ。ここでのブロックは、簡単に「無名関数」を使っていることになる。

EnumerableEnumeratorは繰り返しを抽象化している。例えば、アルファベットの小文字だけで出来ている文字列を、すべて大文字にしたいとする。もちろん、String#upcaseを使えば一発だが、それに頼らず、仮にこんなふうに自力でやってみる。

str = "ruby"
e = str.each_byte
e.map { |b| (b - 32).chr }.join    #=>"RUBY"

このeのクラスがEnumeratorで、each_byteにより(繰り返しのように)1文字ずつ小文字を切り出す。EnumeratorEnumerableを継承しているのでEnumerable#mapが使えるのである。

EnumerableEnumeratorを駆使したプログラミングはとても Ruby らしく、関数型プログラミングっぽい。
 

ブロックを敢て使わない 関数型Rubyプログラミング

yuroyoro.hatenablog.com
ブロックは関数型プログラミングをお手軽に実現する、すばらしい発明である。それだけで、可読性の高い、しかも強力なプログラミングが可能になる。

しかし、ここからは趣味の領域になるが、敢て(あまり)ブロックを使わず、もっと関数型プログラミングを推し進めることもできる。ProcMethodオブジェクトを関数のように使ったプログラミングである。

例えば配列a = [1, 2, 3]の各要素を 2倍して、さらに各々 10を足した配列が得たいとする。もちろん、

a = [1, 2, 3]
new_a = a.map { |i| i * 2 + 10 }    #=>[12, 14, 16]

でいいのだが、あえて演算をバラしてみる。

a = [1, 2, 3]
new_a = a.map { |i| i * 2 }.map { |i| i + 10 }    #=>[12, 14, 16]

 
しかしこれを、Proc(あるいはlambda)を関数のように使って、ブロックの代わりにしてみる。

a = [1, 2, 3]
double = ->(i) { i * 2 }
add = ->(i) { i + 10 }
new_a = a.map(&double).map(&add)    #=>[12, 14, 16]

mapを重ねる代わりに、Proc#>>を使って「関数合成」してもよい。

new_a = a.map(&double >> add)    #=>[12, 14, 16]

関数合成の順番を入れ替えれば、10を足してから 2倍ということになる。

new_a = a.map(&add >> double)    #=>[22, 24, 26]

 

Methodオブジェクト

じつはメソッドも、Procのように「関数化」できる。これはかなり趣味の領域に入ってくるが。

似たことを、Object#methodを使ってやってみる。

a = [1, 2, 3]
def double(i) =  i * 2
def add(i) = i + 10
new_a = a.map(&method(:double) >> method(:add))    #=>[12, 14, 16]

一行def の文法を使った。見てのとおり、かなり趣味的である。

methodメソッドの名前が長ければ、適当にそのエイリアスを定義してもよい。

Object.alias_method(:■, :method)

new_a = a.map(&■(:double) >> ■(:add))    #=>[12, 14, 16]

こうなるともうよくわからない笑。