カリー化と部分適用、原理を説明できますか?
みなさん、こんにちは。どんぶラッコです。
今日は「カリー化と部分適用」というテーマを取り上げてみたいと思います。
前回、zipWith関数の仕組みについて (自分なりに) めっちゃ丁寧に説明しました。
この zipWithの中でもカリー化の説明について軽く触れていますが、今回はそのカリー化と部分適用の意味をHaskellのサンプルを基に説明してみたいと思います。
違う点があったら指摘してください!
また、筆者がJavaScriptに慣れていることもあり、ちょこちょこ「JavaScriptだと〜」とJavaScriptでの実装例もご紹介していきます。
ということで、以下の順に説明していきますね!
関数に名前をつけなくても関数を実行することができる無名関数、及びラムダ式を説明します。
Haskellの原則を基に、高階関数、及びカリー化について解説します。
部分適用の概念について説明します。カリー化の概念があるから、部分適用をすることができるんです!
1. ラムダ式 (無名関数)
1-1. ラムダ式とは?
まずはラムダ式について説明します。
ラムダ式とは、無名関数(名前がない関数)を作るために用いられる記法です。
関数に名前をつけないで直接実行できるので、1回しか使わない関数にいちいち名前を付与せずに直接実行することができます。
記述のルールはこんな感じ。
\引数 -> 関数本体
JavaScriptだと
(引数) => { 関数本体 }
と表記されるやつですね。
ちなみに \引数
のように \
が入るのは λ
を表現しているのだそうです。ややこしい気がする笑
説明だけだといまいちピンとこない方もいると思うので、例をみてみましょう。
引数を渡すと 1 を加算して返してくれる 関数を作りたいとします。 便宜上、 increment
関数とします。
・increment 1
とすると 2 が返ってくる、
・increment 3
とすると 4 が返ってくる
… という具合です。
まずは、通常の関数宣言方法で作ってみましょう。
increment :: Int -> Int
increment x = x + 1
main = print(increment 1) -- 2
これはイメージがつきやすいですね。 2行目が関数の本体です。 x
が引数、 x + 1
が戻り値ですね。
では、これをラムダ式を使って表現するとどうなるか?
main = print((\x -> x + 1) 1) -- 2
一瞬「どういうこと?」と思われる方もいらっしゃるかもしれませんが、仕組みを紐解けば簡単です。
increment
関数を作った場合は、 increment 1
と実行しますね。 increment
の中身は x = x + 1
ですから、つまりこういうことですよね!
increment 1
-- increment が (x = x + 1) になり、 x に 1 が代入される
そして、 increment
を経由することなく、 x = x + 1
という式を評価したい!という需要に応えるのがラムダ式です。
(\x -> x + 1) 1
図にするとこんな感じ。
これがラムダ式・及び無名関数の特徴です。
1-2.ラムダ式を変数に束縛
ちなみに、無名関数と表現したラムダ式ですが、変数に束縛することもできます。
例えば、先程のラムダ式 \x -> x + 1
を increment
で呼び出したい場合、このように表現できます。
increment :: Int -> Int
-- ラムダ式で作った無名関数を変数 incrementに束縛している
increment = \x -> x + 1
main = print(increment 1)
JavaScriptだとこういうことですね!
const increment = x => x + 1
2. 高階関数とカリー化
2-1. Haskellの引数に関する重要なルール
では、ラムダ式を抑えたところで、次の章に移ります。
ここで一つ、重要なルールがあるのでみなさん、絶対覚えてください。
それは、
Haskellには “関数の引数は1つだけ” というルールがある
ということです。この前提がないまま読み進めていっても「??」となってしまいます笑
ちなみに、なぜ Haskell は 1つの引数しか取らないのかというと、Haskellが ラムダ計算(lambda calculus) に基づいて設計された言語であるからです。
ラムダ計算のロジックを説明してしまうと数学の深い話になってしまうので割愛します。興味がある方はこちらの方の講義が大変わかりやすかったのでオススメします。文系の僕でも理解できるくらい平易に説明してくれています。
2-2. 複数の引数と高階関数・カリー化
ここまで読んできて「ん?ちょっと待てよ??」と思った読者の方もいらっしゃるかもしれません。
それは、 Haskellで引数が2つ取れるパターンもあった気がするぞ??ということです。
例えば、 x
と y
を足し算してくれる関数 add
を作りたい!となったら、このように書けますよね
add :: Int -> Int -> Int
add x y = x + y
確かに、2つの引数があるように見えますね。
実際にadd 2 3
を実行すると、5
が返ってきます。
しかし、これも実は厳密に言うと引数が1つしか取っていないのです。
このように、引数を2つ取っている“ように見える”実装をするために必要なのが、高階関数とカリー化という概念です。
まずは、高階関数、カリー化それぞれの言葉の意味を確認しましょう。
関数をインプットとして受け取れる or 関数をアウトプットとして返すことができる関数のこと
複数の引数をとる関数を、引数を1つずつとる関数に変換すること
「なるほど、わからん」となると思うので、先程の add
関数を例に動きを確認していきましょう。
add :: Int -> Int -> Int
add x y = x + y
関数の型を定義している
add :: Int -> Int -> Int
をみていきましょう。
Haskellに慣れていない方のために補足をすると、この部分は関数の名前と、その関数が持つ引数、戻り値を定義しています。
なので、add :: Int -> Int -> Int
を日本語訳すると
“関数add を作ってね、その引数と戻り値はそれぞれ Int(整数), Int, Int 型にしてね”
ということを示しています。
だから、 add x y = x + y
だと、 xはInt型、yはInt型、戻り値 x+y は Int型ということですね。
そして便宜上 x と y をそれぞれ引数と呼んでしまいましたが、この直後でその概念を否定します笑
add :: Int -> Int -> Int
は ()
を使って、このように結合させることができます。
add :: Int -> ( Int -> Int )
先程、関数の引数は1つしか取ることができない、という原則を紹介しましたね。つまり、こんな状態になっているんです!
・引数 Int の戻り値が 関数 (高階関数ですね!)
・戻り値の関数は 引数 Int の戻り値が Int
の2つの関数が実行されているんですね!
実は、 x y = x + y
の部分ですが、第1章で紹介したラムダ式で表現するとこのようになります。
add = (\x -> ( \y -> x + y ))
ということで、 add 2 3
を実行したらどのようなプロセスを経て5
が返ってくるのか、順を追って確認してみましょう。
まず、 add 2 3
が展開され、 (\x -> ( \y -> x + y )) 2 3
の形式になります。
次に、(\x -> ( \y -> x + y )) 2
が実行されます。 x
に 2
が代入され、\y -> 2 + y
という関数が返ってきます。
そして、 (\y -> 2 + y) 3
が実行され、 y
に 3
が代入されます。
結果、5
が返ってくる というわけです。
順番にプロセスを追っていくとそこまで難しくないですよね!
引数を1つずつ取る関数を作りたい(カリー化)、それを実現するために、引数をとると関数を返す関数(高階関数)が必要なんだ!
このように整理するとわかりやすいです。
3. 部分適用
3-1. カリー化するから、部分適用できる
今まで高階関数とカリー化の概念をみてきましたが、これを応用すると 部分適用 というテクニックを使うことができます。
関数を本来の引数よりも少なく渡すことができる仕組みのこと
第1章で例に出した increment
関数を思い出してください。
increment :: Int -> Int
increment x = x + 1
これ、第2章で作った add
関数とそっくりですよね?
add :: Int -> Int -> Int
add x y = x + y
なので、increment
と同等の動きをさせるために、addを活用して add 3 1
、 add 5 1
のように実装をしても良いですが、1は毎回固定なのに都度引数として渡すの、なんかだるいですよね?
そこで登場するのが部分適用です。
add
にあらかじめ引数1を渡しておくことで途中まで関数の処理を進めた状態にできます。
increment
関数を add
関数を使って実装する例を見てみましょう。
add :: Int -> Int -> Int
add x y = x + y
increment :: Int -> Int
increment = add 1
main = print( increment 2 ) -- 3
increment = add 1
の箇所が部分適用ですね!
先程の実行フローに照らし合わせると、この状態です。
つまり、
increment = \y -> 1 + y
と言ってるのと同じ状態であるということですね!
部分適用ができるのは、カリー化ができているから。
この関係性を抑えておけば問題なく理解できるでしょう!
この記事のまとめ
ということで、ラムダ式を紹介した後、カリー化と部分適用についてひと繋ぎに解説をしてみました。
このようにストーリーで考えるとそんなに難しくないですよね!
関数型プログラミングを勉強する上での重要キーワードなので、ぜひこの機会に理解を深めてくれると嬉しいです♪