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

これはカンタン. わかりやすい!

その他書き切れなかったこと

  • if_, else_等も用意した
  • bindが返すProcにはブロックも渡せる(Ruby 1.9以降)
  • 高階関数で関数を引数にとる場合

まとめ

  • Rubyはもともと関数型っぽい
  • 関数型プログラミングを満喫するためにboost::lambda相当のものをRubyで実装
  • 欠点
    • レシーバがふつうのオブジェクトだとうまくいかない
    • 高階関数めんどくさい
  • 欠点を補う手段
    • :symbol[...]記法
    • Proc#curry使え

追記

2011-08-20T02:29+0000 ブックマークコメントへの返信

これを関数型と言われるとなんか違う気がする

「関数型言語」と「関数型プログラミング」を混同しているために生じる違和感ではないでしょうか. 文中の「関数型プログラミング」「関数型」が指しているのはプログラミングのパラダイムとしての関数型であって, とくにfirst-classな関数とimmutableなデータを使うプログラミング技法を指しています.

*1:本当はArray < Enumerable#map.

*2:どうしてこれでうまくいくのか気になる人はこの辺を読もう: Rubyでメタプログラミング 〜暗黙的に呼ばれるto_procメソッド - (゚∀゚)o彡 sasata299's blog

*3:もう少し正確に言うと「1引数でかつ関数全体のλ抽象が左に寄っているもの」である必要があり, なんでもこの方法でいけるわけではない.

*4:Ruby 1.9.2からはProc#curryが標準で定義されている. 今回はRuby 1.8でも使えるように独自にも実装した.