手続き脳のためのラムダ式のはなし

ずいぶん前からメジャーなオブジェクト指向言語ラムダ式が導入されている。
俺がいる職場では未だに.net2.0なので使えないのだが、最近ちょこちょことヘルパーライブラリを作っていてSystem.Funcが使えたらなあーなどと思うことがよくある。職場の何人かに聞いてみると、「ラムダ式ってなんですか」とか、「難しくてよくわからないんですよね」とか、そういう答えしか返ってこなくて、「世の中そういう方向に変わってきているのだから今のうちから触れておかないと気づいたときには手遅れになるぞ」てな話をしたりしている。ま、聞いてくれる人などおらんのだが。
資本主義社会では競争原理が働くので最小の学習コストで最大の収益を得るのは正しいのだが、ソフトウェア企業の社員としてその姿勢はいかがなものか。むしろゴミコードを量産して技術的負の遺産が増えて収益減ってるんじゃないかと思うわけだが(ひどいコード多いし)、そのような話が通じる人たちならそもそもそのような話をするまでもなくすでにそのような状態になっているはずである。

さて、今回は俺みたいな手続き型言語べったりの人間にとっての関数型的な話。Googleで検索すると関数型言語の紹介などは大量に出てくるわけだが、そういう記事を書く人はえてして関数脳をしているので話がさっぱりわからない。逆に関数脳をしていない人がしていない人向けに書いたらもっと役に立たない記事になるのではないかなど思いつつ、そのような記事を書いてみることにした。実験である。
俺はRuby使いなのでここでの説明にはRubyを用いる。

ラムダ式

lambdaと書く。学術的な定義は難しくてよくわからんのだが、要するに引数と計算式を持った式である。Rubyで書くと以下のようになる。

->x{x + 1}

これは引数xを受け取ってx+1を返すラムダ式である。数学的にはf(x)=yというときのfのことで、入力のxが毎回同じなら出力のyも毎回同じになるのが本来の形だろうと思うのだが、Rubyでは式に副作用が無いことは保証できないので、式というよりRubyで言うところのメソッドである。生成時のselfをレシーバとした名前の無いメソッド。これをProcクラスのオブジェクトとして生成するというのがRubyラムダ式である。一般的には関数と言う。
言語によって実現方法は違うだろうけども、なんらかの処理を変数に入れたり、メソッドの引数で渡したり、メソッドから返したりするということができる。

できること

以下の4点。
・関数を生成する
・実行する
・引数で渡す
・戻り値として受け取る
それぞれ順番にいってみよう。

関数を生成する

まあ、さっき書いたのと同じである。いくつか付け加えるなら、引数は複数渡してもいいし、

->x,y{x + y}

変数に入れることもできるし、

hoge = ->x,y{x + y}

中身は複数行書いたりしてもいいし、副作用があってもいい。

->x,y{
  p x
  x + y
}

実行する

[]メソッドで引数を渡して実行することができる。

hoge = ->x,y{x + y}
p hoge[1, 2] #=>3

例では変数に入れているが、別に入れる必要は無い。

p ->x,y{x + y}[1, 2] #=>3

ここまでは特に面白いこともないし、わざわざ生成する意味も感じられない。

引数で渡す

ラムダ式はProcオブジェクトなのでメソッドの引数で渡すことができる。

def foo(b)
  b[1, 2]
end
p foo(->x,y{x + y}) #=>3
p foo(->x,y{x * y}) #=>2

これは何が起こっているのかというと、メソッドfooの中で実行するコードを空白にしておいて、あとでそれを埋めている、という感じである。つまりは何かしらfooの中に定型処理を書いておいて、中核になる部分を後から渡すことで完成させる、しかもそれを差し替えることができる、ということを意味している。これを抽象化と言うのだが、なんだかすごい便利そうな雰囲気になってきた。
しかしながら、Rubyを使っている人にとってはこういう処理は日常茶飯事である。

[1,2,3].each do |v|
 print v
end
#=>123

Array#eachは引数にブロックを受け取り、ループの中でそのブロックを実行する。このブロックとはようするにProcオブジェクトのようなものであり、ラムダ式を受け取ってループの中で毎回実行するといった処理とほぼ同じようなものである。Rubyラムダ式を使った抽象化を部分的に文法でサポートしているのだ。
ちなみにArray#eachをRubyで書くとこんな感じ(適当)になる。

class Array
  def each
    i = 0
    while i < self.size
      yield self[i] #←ここで渡されたブロックを実行
      i += 1
    end
    self
  end
end

このyieldというのが渡されたブロックの実行部分である。そして実際にはArray#eachはもっと多機能だし、きちんとしている。

戻り値として受け取る

引数として渡すことができるなら、当然メソッドから返すこともできる。

def foo
  ->x{x + 1}
end
p foo[5] #=>6

メソッドの中で条件により違うProcを返して、受け取った側で実行するとかすると、ちょっと面白い感じになってきそうだ。とはいえそれは分岐の条件判定と実行のタイミングを変えることしかできないので、言うほど意味は無い。
しかし単純に返すだけじゃなく、なんらかの処理をするProcを生成して返すことができるとしたらどうだろう。

def adder(v)
  ->x{x + v}
end
adder10 = adder(10)  # ←+10する関数を生成する
adder100 = adder(100) # ←+100する関数を生成する
p adder10[5] #=>15
p adder100[5] #=>105

メソッドadderの引数vはラムダ式の中で使われているわけだが、Procオブジェクトは生成時のローカル変数の値を覚えているので、10を渡して生成したProcオブジェクトはadderメソッドから戻ってきても10を足す機能を保持する。こういうのを「ローカル変数vを束縛する」と言うし、Proc外の変数を束縛したProcオブジェクトを「クロージャ」と言う。
この束縛機能を使うといろいろなことができるようになる。

応用1 カリー化と部分適用

さて、変数を束縛することでその変数の値を固定したProcが生成できることがわかった。これを使った基本の応用がある。
以下のようなラムダ式があったとしよう。

hoge = ->x,y{x + y}
p hoge[5,6] #=>11

束縛を利用するとこのように書き換えることができる。

hoge = ->x{->y{x + y}}
p hoge[5][6] #=>11

これは、引数に5を渡した結果として、5+yをするProcが生成されて返ってきて、そいつにyとして6を渡している、と読む。引数を2つとるProcは引数を1つとるProcのネストに書き換えることができる。呼び出し方は少し変わるが、機能は同じだ。この書き換えをカリー化と言う。
カリー化をすると間にProcの生成がひとつ挟まる。ということは、その途中のProcを取り出して保存することもできるということを意味する。

adder = ->x{->y{x + y}}
adder10 = adder[10]
adder100 = adder[100]
p adder10[5] #=>15
p adder100[5] #=>105

adderの2つの引数のうち、片方を固定することができるわけだ。これを部分適用と言う。

応用2 関数を部分適用する

この辺からが関数型プログラミングの醍醐味である。関数脳の人たちはこの辺を自由自在に扱えるのではないかと思うのだが、俺には難しいので、こういう世界があるのだというサワリだけの紹介にとどめたいと思う。
ProcはRubyでは名前の無いメソッドのようなものだと最初のほうで書いたが、メソッドの引数にProcを渡せるのであれば、当然Procの引数にProcを渡すこともできる。

hoge = ->b{b[5, 6]}
p hoge[->x,y{x + y}] #=>11
p hoge[->x,y{x * y}] #=>30

hogeに格納されたラムダ式は5と6に対して「何かする」という関数で、その「何か」の部分を後から渡している。これはずっと上の「引数で渡す」のところで書いたメソッドのProc版である。
5と6を固定してしまったが、これを引数にすることもできる。

hoge = ->b{->x,y{b[x, y]}}
p hoge[->x,y{x + y}][5,6] #=>11
p hoge[->x,y{x * y}][5,6] #=>30

hogeはbだけカリー化してあり、最初の引数に関数を受け取るようにした。もちろんこれらにはワケがある。1個目の引数のみ渡すことで、「2つの引数に対して何かをする」というProcを生成することができる。

hoge = ->b{->x,y{b[x, y]}}
adder = hoge[->x,y{x + y}]
multiplier = hoge[->x,y{x * y}]
p adder[5, 6]      #=>11
p multiplier[5, 6] #=>30

ラムダ式がややこしくなってきたが、冷静に読めば大丈夫なんじゃないかと思う。
このhogeの内側b[x, y]というのが渡されたProcの実行部分で、たとえばこの前後に前処理と後処理を追加したり、ループで囲んだりすることで、いろんな処理が抽象化できるのだ、と言うことができる。
無論、このレベルであればRubyならメソッド定義とブロック渡し、yieldで実現可能なのだが、ブロックを当てはめた処理を保存して渡したり返したりできるという利点がある。
また、関数脳の人はもう一段の抽象化をして、何らか(ここが空白)の処理を抽象化した関数を生成して返すといったことをするのでさらに柔軟で自由度が高く、便利になる。
んでもそこまでいっちゃうと職業手続き脳プログラマ(俺含む)の手に負えないものになるので、ラムダ式を使うとちょっと高度な抽象化ができるんだよ、というところで留めておくのがよさげである。いまのところは。

最後に

Rubyにおいて、ラムダ式はProcオブジェクトのひとつではあるが、procとはちょっと違う。このへんの違いはぐぐると山ほど出てくるのでこういうものを使う場合は調べて違いを覚えておいたほうがよい。
この記事ではそこは主題じゃないのでばっさりカットした。
たぶん、そのへんを知ってる人は読みながらもやもやしたんじゃないかと思う。