「関数型Ruby」という病(2) - 関数合成 Proc#compose
本記事は、Rubyを書くにあたって「いかにブロックを書かずにすませるか」を追求した、誰得な連載である。
注意点として、この記事は、プログラマ厨二病のひとつである「ラムダ症候群(λ-Syndrome)」に罹患した患者にRubyを書かせると、どんなヒドいことになるか実例を示したものであり、けしてこのようなプログラミングスタイルを推奨するものではない。
なぜ関数合成?
まず、なぜ関数合成が必要か、そのモチベーションを示す。
前回、単なるメソッド呼び出しや一引数の関数適用のためだけにブロックを記述する必要はない、という話をした。
だが、以下のようなSymbolのArrayがあり、各要素をto_sした上でupcaseしたい場合はどうするか?
irb(main):003:0> arr = [:user, :entry, :article, :comment, :category] => [:user, :entry, :article, :comment, :category] irb(main):002:0> arr.map{|_| _.to_s.upcase } => ["USER", "ENTRY", "ARTICLE", "COMMENT", "CATEGORY"] irb(main):003:0> arr.map(&:to_s).map(&:upcase) => ["USER", "ENTRY", "ARTICLE", "COMMENT", "CATEGORY"]
上記のように、無念にもブロックを書くか、mapを2回重ねるしかない。ブロックとか書いた時点で負け確定なので何とか死体。
この問題を解決するために、関数合成が必要なのだ。
関数合成の実装
Rubyでは、標準で関数合成ができないので、自前で実装する必要がある。 「Rubyで関数合成したいなら関数型言語使ってろ」なんていう意見もあるのだが、関数をファーストクラスに扱えるのに関数合成がないなんてもったいなさすぎる。
おさらいをしておくと、関数合成とは、 関数gと関数fから、g(f(x))という関数hを新たに作り出すことだ。
(g ∘ f)(x) = g(f(x))
関数gと関数fの合成関数g ∘ fに引数xを渡した結果は、関数gにf(x)の結果を渡したものと等しい。つまり、このような操作である。
irb(main):004:0> f = lambda{|x| x + 1 } => #<Proc:0x007ff6ed91e358@(irb):4 (lambda)> irb(main):005:0> g = lambda{|x| x * x } => #<Proc:0x007ff6ed9123f0@(irb):5 (lambda)> irb(main):008:0> h = lambda{|x| g.(f.(x)) } => #<Proc:0x007ff6ed905ec0@(irb):8 (lambda)>
Procに対して、関数gを引数に取って、自身との合成関数を返すメソッドcomposeを以下のように定義する。
module ComposableFunction def compose(g) lambda{|*args| self.to_proc.call(g.to_proc.call(*args)) } end def >>(g) g << self end def self.included(klass) klass.send(:alias_method, :<<, :compose) end end [Proc, Method, Symbol].each do |klass| klass.send(:include, ComposableFunction) end
"<<"はcomposeのaliasで、">>"は引数の順序を入れ替えたcomposeだ。">>"と"<<"は関数適用の流れを示している。これはGroovy由来だ(Scalazでは>>>らしい。)
gとfの合成関数は、「関数fの結果を関数gに渡す関数」と言えるので、この定義通りに"f >> g"と書くとgとfの合成関数が得られるようになっている。
冒頭の例で出した、「to_sしてからupcaseする」には、:to_s.to_procで得られるProcと、:upcase.to_procで得られるProcを">>"で合成すればよい。
irb(main):067:0> f = :to_s.to_proc => #<Proc:0x007ff6ed924848> irb(main):068:0> g = :upcase.to_proc => #<Proc:0x007ff6ed924690> irb(main):069:0> h = f >> g => #<Proc:0x007ff6ec020360@(irb):47 (lambda)> irb(main):070:0> h.(:abc) => "ABC"
そして、このようにして得た合成関数hを、arrのmapメソッドに渡せば、mapを重ねることもなく、ブロックを書くこともなく目的を達成できる。
irb(main):072:0> arr.map(&h) => ["USER", "ENTRY", "ARTICLE", "COMMENT", "CATEGORY"] irb(main):073:0> arr.map(&:to_s >> :upcase) => ["USER", "ENTRY", "ARTICLE", "COMMENT", "CATEGORY"]
予め合成した関数を変数に入れておいてもいいが、mapに渡す際にmap(&:to_s >> :upcase)のように合成してもよい。SymbolにもComposablefunctionをincludeさせているので、合成にあたってto_procを呼び出していちいち変換しなくてもよいような仕掛けにしてある。
本来は、map{|sym| sym.to_s.upcase} と書いていた処理がmap(&:to_s >> :upcase)に変換できた。これは、メソッドチェーンを行うようなブロックはSymbol#to_procと関数合成により置換可能であることを示している。
Object#methodと組み合わせることもできる。
irb(main):075:0> arr.each(&:to_s >> :upcase >> method(:puts)) USER ENTRY ARTICLE COMMENT CATEGORY => [:user, :entry, :article, :comment, :category]
余談だが、arr.map(f).map(g)とarr.map(g compose f) が等しくなるのは、名前を言ってはいけないあの法則(モニャーーーッ!!)が思い出される。よって、合成される関数が副作用を伴う場合は注意したい。副作用は滅べばいい( ;゚皿゚)ノシΣ フィンギィィーーッ!!!
次回は、カリー化について書く( ꒪⌓꒪)