Edited at

lambda_driverを使いこなすために

More than 5 years have passed since last update.


yuroyoro/lambda_driver

Rubyで関数合成とかしたいので lambda_driver.gem というのを作った - ( ꒪⌓꒪) ゆるよろ日記


前回のRuby - lambda_driverがカッコいい - Qiitaは書いたコードを載せただけだったので、今回はlambda_driverを使えるようになるための文章を書いてみる。

全てはカッコいいコードのために!


方針

lambda_driverを使いこなす上で避けては通れないのが関数合成です。

そして関数合成するには、


引数を1つ取って、返り値を1つ返す関数

である必要があります。

aからbを作る関数とbからcを作る関数を1つにするので、必然的に上記のような制限があるのですね。(a,b,cは、Arrayなどのように1つの変数に入れられるものならばなんでもよい)

Rubyには様々なオブジェクトを関数のように扱う方法があり、それらを使って関数を扱います。


まずはProc

関数オブジェクトと言えばProcです。


proc.rb

Proc.new {|x| x + 5}

proc {|x| x + 5 }
lambda {|x| x+ 5 }
->(x) { x + 5 }

複数の作り方があり、いくつかの違いはあれどどれもcallによって実行できます。


exe.rb


pr = proc {|x| x + 5 }

pr.call(3) #=> 8

pr.(3) #= 8


pr.(3)はcallへのシンタックスシュガーです。

 

Procはそのまま(上記の1入力1出力であれば)関数合成ができます。


compose.rb


plus = proc {|x| x + 5 }

double = proc {|x| x * 2 }

plus >> double #=>Procオブジェクト


Proc#>>は関数合成メソッドで、lambda_driverの中で


code.rb

class Proc

def >>(g)
lambda {|arg| self.to_proc.call(g.to_proc.call(arg) }
end
end

のような感じ(本当の実装はこれとは違いますがイメージとして)に実装されています。

「gを受け取って、「argを受け取って、gで処理してからselfで処理して返す関数」を返す関数」です。

Proc#to_procは自分自身を返します。つまり何もしません。

しかし、gに後述のSymbolがくることがあるのでto_procしています。


Method(Object拡張)

普段あまり使うことのないMethodオブジェクトですが、defで定義したメソッドを関数オブジェクトにするときには使います。

defで定義したメソッドの名前のSymbol(Stringでもいいのですが慣習としてあまり使われない)をObject#methodに渡すことで作ることができます。


method.rb

class Piyo

def piyo(x)
"piyo" * x
end
end

p = Piyo.new.method(:piyo) #=> Methodオブジェクト
p.(2) #=> "piyopiyo"

def plus(x)
x + 5
end

m = method(:plus) #=> Methodオブジェクト

m.(2) #=> 7


クラス定義内ではselfはクラス自身、トップレベルでのselfはObjectクラスであるmainであるので、レシーバーを省略して

method(:plus).(2)

と書けます。

そして、lambda_driverではObject#methodはObject#_にショートカット(というか実装的にはrespond_to?とかOpProxy.new(self)とかするメソッド)しているので、このコードは


method.rb

class Piyo

def piyo(x)
"piyo" * x
end
end

p = Piyo._.new #=> Methodオブジェクト
#Piyo._の時点でmethod_missingをいじって指定された名前(ここではnew)のMethodオブジェクトを返すようなオブジェクトにしてる
#詳しくはgithubのlambda_driver/lib/lambda_driver/op.rb参照

#こういう風に、Object#methodのエイリアスの様にも書ける
p = Piyo._(:new) #=> Methodオブジェクト

p.(2) #=> "piyopiyo"

def plus(x)
x + 5
end

#method(:plus)と書いた方が良い気もするけど一応
m = self._.plus #=> Methodオブジェクト
m.(2) #=> 7


と書けます。

インスタンスメソッドやクラスメソッドを関数合成する時に使います。

前回のコードで言えば


usage.rb

require 'rexml/document'

def validate(doc)
#問題がなければそのまま返す
doc
end

make = REXML::Document._.new >> method(:validate)
#makeはStringを受け取って、validateするproc

#インスタンスにも使えるよ
"piyo"._.capitalize #=> "Piyo"


のような感じです。


Symbol

Symbolはto_procすることで、Procにすることができます。


symbol.rb

:to_s.to_proc


lambda_driverの関数合成は、先ほど書いたようにto_procを勝手に呼んでくれるので、


symbol.rb

p = :to_s >> :length #=> Procオブジェクト

p < :piyo #=> 4
# Proc#< はcallへのエイリアス

と、まるで関数そのものであるように関数合成できます。


Symbol#to_method_with_argsがスゴイ

今まで、「引数のメソッドを引数を渡して実行する関数オブジェクト」を作るにはprocで頑張って書くしかなかったのですが、Symbol#to_method_with_argsを使えば簡単に書けます。


&.rb

getter = :get_elements & "nicovideo_thumb_response/thumb"

# &はto_method_with_argsのエイリアス

getter.(doc) # これでdoc.get_elements("nico~")してくれる



関数合成のススメ

selfからdefで定義したメソッド(1引数)は method(:mtehod_name)

クラスメソッド、インスタンスメソッド(1引数)は class_name._.method_name 及び obj._.method_name

引数なしのレシーバーのメソッドは :method_name

引数と共に使うレシーバーのメソッドは :method_name & args

によって、関数合成可能になります。

この辺りを覚えておけば、だいたいのものは関数合成できるようになるのではないでしょうか。

method,to_procなどを使ってProcにして、Proc#with_args, Proc#flipを使えば複数引数にも対応可。


まだまだあるけど

lambda_driverにはまだ|>,disjunction,curry,liftなどがありますが、またの機会に。


楽しめば良いんじゃないかな

lambda_driverは関数型のコーディングを強力に推進してくれますが、知らない人からすると黒魔術です。

仕事で使うような代物ではないので、個人で楽しく書けば良いと思います。

ここまで読んでいただきありがとうございます。

そしてlambda_driverの開発者のyuruyoroさんに最大限の感謝を。