LoginSignup
5
3

More than 3 years have passed since last update.

関数型プログラミング入門者向け。Scalaと関数型プログラミングで一番重要な概念: Part 2 部分適用とカリー化

Last updated at Posted at 2019-08-16

関数型プログラミング入門者向け。Scalaと関数型プログラミングで一番重要な概念: Part 1 関数合成 からの続きです。

Part 2 部分適用とカリー化

部分適用とカリー化は混同されがちだけど、違うものです。部分適用はカリー化とは関係なく出来ますし、カリー化された関数は部分適用を使わずに利用することができます。一緒に使われることが多いようですが、お互いに依存はしていみせん。

混乱を解きほぐすために、まずはカリー化の話題は後回しにして、伝統的な複数の引数を取る関数を使って部分適用をみていきます。次に、カリー化された関数の定義の仕方をおさらいします。その後、カリー化された関数と部分適用について解説します。そして最後にカリー化された関数と合成関数についてみていきます。

高階関数は部分適用や関数合成とともに利用されることが多いです。その際、カリー化された関数が使わる場合もあるし、無い場合もあるでしょう。高階関数は別に解説する予定です。

複数の引数を取る関数を特殊化(specialize)する

部分適用機能を使って、ある関数の引数の(すべてではなく)いくつかを指定することで、新しい関数を定義できます。

この機能の使い方を、まずは1つ以上の引数を取る関数を使ってみていきましょう。

引数を3つ取る、ごく簡単な f という関数を定義します。

def f(a: Int, b: Int, x: Int):Int = a * x + b

関数の呼び出しはいつもどおり。

scala> f(2, 5, 3)
res81: Int = 11

さてここで、引数 a の値が 2 であるとが、早い段階で分かっていたならば、以下ように出来ます。

scala> def f2 = f(2, _, _)

scala> f2(5, 3)
res85: Int = 11

ここでは、アンダースコア記号は引数 bx への未知の値を表します。対して、引数 a については値 2 を関数 f に部分適用して新たな関数を作り出し f2 という名前で定義しました。関数 f2 の呼び出すには引数は2つだけ渡せば済みます。

さらに別の状況を見てみましょう。もし引数 b の値だけがすでに分かっているとしたらどうでしょう? (値は 5とします)

多分、すぐに分かったと思いますが・・・このようになります。

scala> def f3 = f(_, 5, _)
f3: (Int, Int) => Int

scala> f3(2, 3)
res84: Int = 11

f3 は2つの引数を取りますが、それは ax に対応します。値 5 はすでに b の引数として固定されています。

では、 ab の両方の値がすでに分かっていて、未知のものは x だけだった場合、どうなるかはもうすでにお分かりでしょう。

scala> def f4 = f(2, 5, _)
f4: Int => Int

scala> f4(3)
res86: Int = 11

カリー化の仕方

まずは、前のセクションで使った関数をカリー化された状態にすることから始めます。

ここで改めて確認ですが、カリー化と部分適用はそれぞれ独立した概念であり、一緒に利用される場合もあるけれど、依存関係にはありません。

Scala では、カリー化された関数は、このように記述できます。

scala> def fc(a:Int)(b:Int)(x:Int) = a * x + b
fc: (a: Int)(b: Int)(x: Int)Int

カリー化された関数 fc の実際の振る舞いは、前述の複数の引数を取る関数 f と全く同じです。

すぐに気づく点は、引数の指定の仕方です。a, b, そして xそれぞれの引数が独立したカッコのペアで囲まれています。こうすることで、関数 fc は引数を1つだけとり、戻り値として別の関数を返すようになります。

分かりやすいように、複数の引数を取る関数 f とカリー化された関数 fc を比較してみましょう。

scala> f _
res96: (Int, Int, Int) => Int = $$Lambda$1172/0x0000000801894040@ffa7bed

scala> fc _
res97: Int => (Int => (Int => Int)) = $$Lambda$1173/0x0000000801893840@4e843bf6

複数の引数をとる関数の方はよく見慣れた感じです。 (Int, Int, Int) => Int の部分は 「この関数はInt型の引数を3つ取り、Int型の戻り値を返す」 を意味します。

カリー化された関数(引数を1つしかとらない方) は様子が随分と異なります。 Int => (Int => (Int => Int)) の部分は 「この関数はInt型の引数を1つ取り、戻り値として(Int => (Int => Int))型の関数を返す」 という意味になります。

実は、このブログ記事はもともと英語で書いたんですが、このカリー化された関数の型を表すにい日本語は大変不向きです。英語で表現するならもっと自然です。

it is a function that takes a single argument of Int and returns a function that takes a single argument of Int type and returns a function that takes a single argument of Int type and returns a value of Int type."

(読み方のコツは、=> を見つけたら => の左側について「a function that takes... and returns (a function that...)」と始めることです。英語だとうまく入れ子の状態とマッチして文に出来ます。)

ここで鍵になる考え方は 「・・・引数を1つ取り・・・」という部分です(英語の"...takes a single argument..."の部分。英語では引数の数と同じだけ、"...takes a single argument..." という表現が出現しているのが分かると思います(日本語だと難しい・・・)。

ということで、関数 fc は引数を1つ求めているので、ある値を渡したとすると、戻り値として(Int => (Int => Int))という型の関数を返します。

カリー化された関数では、2つ以上の引数を一度に渡すことはありませんし、それは入れ子状態の関数のどのレベルでも同じです。(2つ以上の値を「組(tuple)」として渡す場合はあるかもしれませんが、それでも引数としては1つです)

寄り道:JavaScriptでカリー化された関数を見てみよう

JavaScript で同じ関数をカリー化して利用することも出来ます (この例は Google Chrome の開発者向けツールのコンソールに直接入力しました).

> var fc = a => b => x => a * x + b
< undefined
> fc
< a => b => x => a * x + b

もしかしたら、伝統的な function 予約語を使って定義したバージョンの方が理解しやすいかもしれません。

var fc3 = function(a) { 
    return function(b) { 
        return function(x) { 
            return a * x + b 
            } 
        } 
    }

ところで、引数 a の値が、最内部の関数の本体からもアクセスできるのはクロージャー(closure) のおかげです。引数 b に関しても同じです。そうやって式 a * x + bab の値が渡されます。クロージャは JavaScript でカリー化を可能にさせる機能なのです。

2つの関数 fcfc3 は振る舞いとしては全く同じですし、両方ともカリー化されているので、呼び出し方法も同じです。(記述の仕方としては fc の方がずっと簡単ですね)

fc(2)(5)(3)
11
fc3(2)(5)(3)
11

カリー化された関数は引数を1つしかとらないので、それぞれの入れ子になった関数に対しても同様に引数は1つしか渡しません。そのため (2), (3), そして最後に (3) という形で引数を別々に渡しています。

では、JavaScript の世界を離れて、Scala の世界に戻りましょう。

Scala でのカリー化された関数の呼び出しもこのようになります。

scala> fc(2)(5)(3)
res99: Int = 11

特に驚きの要素はありませんね。記述の仕方としては、引数として1つ渡すというのは変わらないので、JavaScriptの場合と全く同じです。

複数の引数を取る関数をカリー化する

すでに存在する複数の引数を取る関数を、簡単にカリー化できるとしたら便利ですよね。幸運にも Scala にはビルトインで簡単にカリー化できる機能があります。

以下では、3つの引数を取る関数 f のカリー化されたバージョンを fc という名前を付けて定義しています。

scala> def fc = (f _).curried
fc: Int => (Int => (Int => Int))

scala> fc(2)(5)(3)
res103: Int = 11

(f _) の部分は関数 f そのものを表していて、関数を呼び出さないようにしています。アンダースコアを指定することでインタープリターが関数呼び出しを実行することを防いでいます。curried というメンバーからカリー化された関数を得ることが出来ます。

カリー化された関数から特殊化(specialized)された関数を作る

kの段階は、カリー化された関数からどのように特殊化された関数を作るか、という話題はもう必要ないでしょう。もうすでに何度も例を見てきましたから。

以下では、引数ab に値を渡し(ただしxに関しては未知のままとする)、新たに fc4 という名前の特殊化された関数を作ります。 関数fc4 を呼び出す際は、引数 x に対する値を渡すだけです。

scala> def fc4 = fc(2)(5)
fc4: Int => Int

scala> fc4(3)
res104: Int = 11

カリー化された関数で関数合成をする

andThen メソッドとcompose メソッドのおさらい

すでに andThen メソッドを使って関数を合成するやり方を見てきましたが、compose メソッドを使って同じように関数を合成できます。このセクションでの例はcompose メソッドを利用するので、その使い方をまず確認しておきましょう。

以下のような2つの関数があったとします。

scala> def inc(x:Int) = x + 1
scala> def mulBy2(x:Int) = x * 2

この二つの関数を mulBy2(inc(x)) ような関係にしたいとします。例えば、 x=1 の時 4 が結果になるように、または x=2 の時 6 が結果になるような組み合わせです。

compose メソッドを使うと、このように関数を合成できます

scala> def c1 = (mulBy2 _).compose(inc _)

関数 c1 を呼び出して、正しく合成されているか確認します。

scala> c1(1)
res206: Int = 4

scala> c1(2)
res207: Int = 6

同様に逆の関係 inc(mulBy2(x)) にするには以下のように組み合わせます(この場合 x=1 なら結果は 3 で、x=2 なら結果は 5 になるはずです)

scala> def c1 = (inc _).compose(mulBy2 _)

つまりコンポーズする関数はコンポーズされた関数の出力をその入力にとるわけです。

以下、compose メソッドを利用して、先に定義したカリー化された関数 fc (下参照)を改造していきます。

def fc(a:Int)(b:Int)(x:Int) = a * x + b

ではちょっとシナリオを設定しましょう。この関数 fcは引数を3つとるわけですが、 ab 関して、以下のようなパラメータ検証をすることになったと仮定します。

  • 引数a の値は常に絶対値に変換してのち、fc に渡さなくはならない (ヒント:Math.abs ライブラリ関数が使えそうです)
  • 引数b の値は5以下でなくてはならない (ヒント: Math.min ライブラリ関数が使えそうです)

これらを式として表すとこうなります:

Math.abs _
def atMost5 = Math.min(5, (_:Int)) // ヘルパー関数を作成しました

ヘルパー関数 atMost5 がきちんと動くかテストしておきましょう(高階関数 map はこんな時にも便利です)

scala> (1 to 10) map(atMost5)
res8: IndexedSeq[Int] = Vector(1, 2, 3, 4, 5, 5, 5, 5, 5, 5)

入力が5より大きい時でも出力は5になっています。

では、引数 ab の値それぞれに事前処理を施しつつ、関数 fcと合成してみましょう。まずは引数 a から初めて、段階を追ってみていきます。

scala> (fc _).compose(Math.abs _)
res10: Int => (Int => (Int => Int)) = // 右辺は省略しました

この合成のパターンは fc(Math.abs(a)) という関係に2つの関数を結び付けます。ここで気を付けなくてはいけないのは、この合成された関数はあくまでも まだ3つの引数を取る関数 であるということです。決して 2つの引数を取る関数にはなっていません - それは部分適用の話です。これは関数合成の話であって、どの引数にもいまだ値は適用されていません。重要な点は、後の実行時に、引数a として渡される値は、合成された関数 Math.abs通って fc に到達する、ということです。

次に、2つ目の引数 b に対する事前処理(最大で値は5になるように強制する)を行うように関数を合成しましょう。この場合、b へ値がヘルパー関数 atMost5通り抜けていく ように構成しなければなりません。つまり、1つ目の引数をなんとかスキップしなくてはならないのです。

scala> (fc _).compose(Math.abs _)(_:Int).compose(atMost5)

注目してもらいたいのは (fc _).compose(Math.abs _) の部分のすぐあとに来る (_:Int) の部分です。これは1つめの引数は「未知のまま」としてスキップしていて、2つ目の引数に対して - 言い換えると「2つ目の引数を受け取る、1つしか引数を受け取らないカレー化された関数」に対して - compose メソッドを使って atMost5 と結び付けているのです。

では、この合成された関数に名前をつけて新し関数として定義しましょう。

scala> def fcx = (fc _).compose(Math.abs _)(_:Int).compose(atMost5)
fcx: Int => (Int => (Int => Int))

合成関数 fcx はいまだにトータルで引数を3つ取ることは変わりません(=> 記号の左側が引数ですから、数えるとInt が3つあります)。まだ引数の値はひとつも渡していないので、ベースの関数 fc は引数を3つ必要とします。 (ヘルパー関数 atMost5 に対して値5を部分適用して固定しましたが、これは fc 関数に対する部分適用ではありません)。

ベースになったカリー化された関数 fc を一切変更することなく、2つの引数が事前処理されるように構成することができました。

同様のことを命令型プログラミングでやるとどのようになるでしょう。fc 関数本体を変更するやり方もあるでしょうし、呼び出しの際に、インラインで事前処理の関数を呼び出す方法もあるでしょうし、一種の関数合成として別名を与えることも出来ます。ただ、はたしてそれが好ましい方法なのかどうかは議論の余地があるでしょう。

関数型プログラミング、さらにはカリー化と合成関数は、このような特殊化(specialization)が普通に起こるという状況で、その柔軟性を発揮することが出来ます。

では実際にテストしてみましょう。テストケースは3つ用意します。

  • fcx(2)(5)(1) は、 2 * 1 + 5 なので結果は 7になる。
  • fcx(-2)(5)(1) は、まず -2Math.abs によって 2 に変換され、2 * 1 + 5 となるので、結果は 7になる。
  • fcx(2)(8)(1) は、まず b=8atMost5 によって強制的に b=5 になり、2 * 1 + 5となるので、結果は 7になる。

テスト結果:

scala> fcx(2)(5)(1)  
res27: Int = 7       

scala> fcx(-2)(5)(1) 
res28: Int = 7       

scala> fcx(-2)(8)(1) 
res29: Int = 7       

期待通りの結果になりました。

カリー化された関数の利点とは?

オンラインでこの答えを探すと(少なくとも英語圏では)あまり統一の取れた見解はでてきませんでした(もちろん、調査する私の力不足も含めての結果ですが・・・)。多く見られるのが、部分適用の利点をカリー化の利点としてしまっている主張です。ですが、この記事でも解説した通り、部分適用はカリー化を必要しないので、部分適用の効果をカリー化の効果とするのは正しくありません。

ですが、カリー化(とカリー化された関数)の利点を、部分適用とは別けて解説しているブログをとりあえずひとつ見つけました。 article by Iven Marquardt。この記事によると、カリー化の利点は

abstracts the variations of number of arguments (引数の数の違い・バリエーションを抽象化する)

とあります。これはなるほどと納得できるものです。関数を合成しようとして、よく遭遇する「壁」として、引数と戻り値の型の他に、引数の数があります。うまく合成してパイプライン、またはツリー構造に構成したくとも、既存の関数が複数の引数を取るためにうまくいかなかったりします。ですが、カリー化は「引数は常にひとつ」という状態を保証するので、粒がそろうことで関数合成がしやすくなります。

関数型プログラミング入門者の私にとって、もっとも納得いく説明はこのようなものでした。

5
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3