Rubyで関数型プログラミング
Rubyでの快適関数型プログラミングライフを追求するあまり, 使えるのか使えないのかよくわからないものを作ってしまったという話. Rubyに不慣れな人や関数型プログラミングに不慣れな人に対して酷なのはまだわかるとしても, C++(というかboostでの関数型プログラミング)に不慣れな人も全力で置いてきぼりにする誰得記事になってしまった......
経緯
そもそもRubyはだいぶLispっぽくて, ブロックとイテレータを使うだけで関数型プログラミングになってしまう. たとえばこんな感じで:
%w|1 2 3 4 5|.map{|x| x.to_i}
%w|1 2 3 4 5|
は'1'
から'5'
までの文字列からなる配列で, その配列のメソッドArray#map
*1に対して, 受け取った引数を整数化するブロックを渡してやると, 1
から5
までの整数からなる配列が得られる. たとえば関数型プログラミング言語として名高い(?)OCamlで書くなら
List.map (fun x -> int_of_string x) [ "1"; "2"; "3"; "4"; "5" ]
という感じ. メソッド名やクロージャの作り方や引数の順序などの記法上の差異を除けばよく似ている. 実に関数型っぽい.
最初から関数型っぽさの漂うRubyに対して少し文句があるとすれば, せっかくオブジェクト指向でもあるのに, いちいちx
を用意しておいてx
に対してto_i
を呼ぶのがいけてないということくらいだろうか. 配列の各要素に対してto_i
しろって言ったらレシーバはその要素に決まってんだろ空気読め, みたいな.
と思いきや, これは実は最初からできる.
%w|1 2 3 4 5|.map(&:to_i)
と書くと期待通り動く*2. なにこれべんり.
ただこれでもまだ不満があって,
%w|1 2 3 4 5|.map{|x| x.to_i * 10}
みたいなのを簡単に書けない. べつにto_i
が悪いわけではなくて,
[1, 2, 3, 4, 5].map{|x| x * 10}
だったとしても簡単に書けない.
[1, 2, 3, 4, 5].map(&:*)
というのは構文として正しいし, きちんと*
メソッドの呼び出しをProc
化してくれるけれど, 10
を渡すことができていない. C++での関数型プログラミングに慣れている人ならば思うことだろう, boost::bindが欲しい, と. 欲しければ作ってしまおう. さらに欲張ってboost::lambda相当のものを作ってしまおう.
できたものと基本的な使い方
require 'functional/lambda' Lambda.eval do [1, 2, 3, 4, 5].map(& _1 * 10) end
ブロックで囲むのめんどくさい
include Lambda::Primitive include Lambda::Variable include Lambda::Statement
としておくと, Lambda.eval do ... end
で囲まなくても_1
等が使えるようになる. この後の例ではLambda.eval do
, end
は省略.
_1
って書くのすらだるい
_
と書いても_1
の意味になるようにしてあるので, 引数が1つしかないなら1
は書かない方が見やすいかも. ただし, _
が既に定義されていると使えない(irbなんかでは定義されてしまっているっぽいので使えない).
具体例
%w|1 2 3 4 5|.map(& _1.to_i * 10) # => [10, 20, 30, 40, 50]
map
によって各要素が_1
の部分に渡ってくる.
[1, 2, 3, 4, 5].map(& _1 * _1) # => [1, 4, 9, 16, 25]
2乗を計算.
(_3 - _2 * _1).call(10, 20, 30) (_3 - _2 * _1)[10, 20, 30] # => -170
30 - 20 * 10
を計算. (_3 - _2 * _1)
の部分はProc
なので[]
でも呼び出せる.
制限
実際の実装は以下のような方針.
- まず
bind
を定義 _1.method(*args)
は,_1
のmethod_missing
の中で:method.to_proc.bind(_1, *args)
に変換
このことからすぐにわかるように, メソッドのレシーバが_1
等, このライブラリで管理しているオブジェクトでないと, bind
が呼ばれない.
たとえば
[1, 2, 3, 4, 5].map(& 10 * _1)
は動かない!
こういうときのために, いちおう代替的な記法があって,
[1, 2, 3, 4, 5].map(& :*.to_proc.bind(10, _1))
とすればいける. ただいくらなんでもこれは長すぎるので,
Lambda::Syntax::Method.from_symbol[] # 記法を有効にするおまじない [1, 2, 3, 4, 5].map(& :*[10, _1])
と書けるようにしてある.
高度な使い方 - 高階関数
関数型と言うからには高階関数も使いたい. 関数を返す関数も書きたい. つまりラムダ計算で言うところの
λx.λy.λz.z-y*x
みたいなのが書きたい. 「_1
とか_2
ってde Bruijn indexなんでしょ, 簡単じゃん!」と勘違いしそうだけど, _1
は(1階の)多引数関数の1番目の引数という意味なのでde Bruijn indexではない. だから
(_3 - _2 * _1)[10][20][30]
と書いてもうまくいかない.
関数を返す関数をサポートするにあたっては, boost::lambdaに倣ってprotect
という関数を用意した.
(protect(protect(_1)) - protect(_1) * _1)[10][20][30] # => -170
関数が呼び出されたときにprotect
された変数には代入されない代わりにprotect
が1回剥れる. 平たく言うと段階計算みたいなことが起きる.
(protect(protect(_1)) - protect(_1) * _1)[10][20][30] # -> (protect(_1) - _1 * 10)[20][30] # -> (_1 - 20 * 10)[30] # -> (30 - 20 * 10) # -> -170
ただ, 厄介なことに, この段階計算の途中でメソッドのレシーバがふつうのオブジェクトになってしまうと, 制限のところで説明したのと同じ問題が発生してうまくいかない.
(_1 - protect(_1))[10][20] # -> (10 - _1)[20] # -> TypeError!
で, ふつうだったら-
のところを:-[...]
に変えればいいけれど, いまは段階計算しているのでそれでもうまくいかない.
:-[_1, protect(_1)][10][20] # -> (_1 - protect(_1))[10][20] # -> (10 - _1)[20] # -> TypeError!
計算の段階が1つ進んだ後で:-[...]
をしないといけない. ということは, :-
に対して[...]
を呼ぶところをprotect
しないといけない.
:send[protect(:-), :[], _1, protect(_1)][10][20] # -> :-.send(:[], 10, _1)[20] # -> :-[10, _1][20] # -> 10 - 20 # -> -10
うん, これは, なんだ, キモい. なによりも, 問題の在処がわかった瞬間にこれをすらすら書いてしまった自分が恐しい.
こんな面倒な段階計算スキルを身につけなくても, 1引数の高階関数しか使わないなら*3, 多引数関数をカリー化*4してしまえばいい.
(_3 - _2 * _1).curry[10][20][30] # => -170 (_1 - _2).curry[10][20] # => -10
これはカンタン. わかりやすい!
まとめ
- Rubyはもともと関数型っぽい
- 関数型プログラミングを満喫するためにboost::lambda相当のものをRubyで実装
- 欠点
- レシーバがふつうのオブジェクトだとうまくいかない
- 高階関数めんどくさい
- 欠点を補う手段
:symbol[...]
記法Proc#curry
使え
追記
2011-08-20T02:29+0000 ブックマークコメントへの返信
これを関数型と言われるとなんか違う気がする
「関数型言語」と「関数型プログラミング」を混同しているために生じる違和感ではないでしょうか. 文中の「関数型プログラミング」「関数型」が指しているのはプログラミングのパラダイムとしての関数型であって, とくにfirst-classな関数とimmutableなデータを使うプログラミング技法を指しています.