カリー化と部分適用について解説

Scala 3 (Dotty 0.26.0-RC1) 2.13.3 2.12.12
最終更新:2020年7月11日

[AD] scalapediaでは記事作成ボランティアを募集しています

カリー化と部分適用は混同されやすいですが違うものです。
この記事ではカリー化と部分適用について解説します。

カリー化

「カリー化」とは何でしょうか?
ここでは、関数をカリー化する意味とScalaで関数をカリー化する方法を説明します。

複数の引数を持つ関数をカリー化すると関数を返す関数になる

「カリー化」とは、複数の引数を受け取る関数を以下のように別の形の関数に変換することです。
カリー化を行うと、元の関数は、最初の1つの引数を受け取って「残りの引数を受け取る関数」を返す関数へと変換されます。

例えば、f(a, b) -> cという関数 fを、F(a) -> f1(b) -> cのような関数 Fに変換することをカリー化といいます。

関数 Fは、関数 fの最初の引数 aを受け取り、関数 f1を返します。
関数 f1は、関数 fの残りの引数 bを受け取り、値 cを返します。

次に以下のように3つの引数を受け取る関数をカリー化してみます。
f(a, b, c) -> d

この関数をカリー化すると以下のようになります。
F(a) -> f1(b) -> f2(c) -> d

また、以下のように最初の引数「a」を受け取り残りの引数「b」「c」を受け取る関数に変換することもカリー化です。
F(a) -> f1(b, c) -> d

「第一級関数(first-class function)」を扱うことができるプログラミング言語であればカリー化することができます。
ScalaはもちろんJavaScriptRubyPythonも関数をカリー化することができます。

カリー化された関数を定義する

次にScalaでカリー化する方法を説明します。

以下は2つのIntを受け取り2つの引数を足した値を返す関数です。
この関数をカリー化してみます。

val nonCurriedSum = (a: Int, b: Int) => a + b

上記関数をカリー化すると以下のようになります。
この関数は、最初の引数「a」を受け取り、残りの引数「b」を受け取り「a」と「b」を足した値を返す関数を返します。  

val curriedSumFunc = (a: Int) => (b: Int) => a + b

メソッドはカリー化した状態で定義することができます。
メソッドの場合は、以下のように引数ごとにカッコ()で囲むことでカリー化します。

def curriedSumMethod(a: Int)(b: Int) = a + b

定義した関数とメソッドを呼び出してみます。

println("nonCurriedSum(1, 2) = " + nonCurriedSum(1, 2))
println("curriedSumFunc(1)(2) = " + curriedSumFunc(1)(2))
println("curriedSumMethod(1)(2) = " + curriedSumMethod(1)(2))

実行結果は、以下の通りすべて同じ値になります。

nonCurriedSum(1, 2) = 3 curriedSumFunc(1)(2) = 3 curriedSumMethod(1)(2) = 3

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

「カリー化された関数を定義する」では、カリー化した関数の定義方法を説明しました。
Scalaには、2つ以上の引数を受け取る関数をカリー化するメソッドがあります。
2つ以上の引数を受け取る関数にはcurriedメソッドが用意されており、このメソッドを呼び出すと関数をでカリー化することができます。

「カリー化された関数を定義する」で定義した「nonCurriedSum」も以下のようにcurriedメソッドを呼び出すことでカリー化することができます。
以下の関数「curriedSum」の型は、「Int => Int => Int」です。

val curriedSum = nonCurriedSum.curried println("curriedSum(1)(2) = " + curriedSum(1)(2))

実行結果は、以下のとおりです。

curriedSum(1)(2) = 3

メソッドはcurriedメソッドでカリー化できないので注意が必要です。

部分適用

次に部分適用について説明します。

部分適用とは関数の一部の引数を適用したもの

関数の一部の引数を適用して新たな関数を作成することを部分適用と呼びます。
「カリー化した関数」と「複数の引数を受け取るカリー化されていない関数」の両方に対して部分適用することができます。

カリー化された関数を部分適用する

以下のカリー化された関数を部分適用してみます。

val curriedDivFunc = (a: Float) => (b: Float) => a / b

関数「curriedDiv」に対して最初の引数のみを適用して関数「g」を取得します。
この関数「g」が部分適用された関数です。

val g = curriedDivFunc(10)

部分適用された関数「g」に残りの引数を適用します。

println("10 / 2 = " + g(2))

実行結果は以下のとおり部分適用した際に渡した値「10」を部分適用された関数「g」の引数で指定した値で割った値が返ります。

10 / 2 = 5.0

メソッドを部分適用する場合は、以下のように省略する引数にアンダースコア(_)を指定する必要があります。

def curriedDivMethod(a: Float)(b: Float) = a / b val g = curriedDivMethod(10)(_)

実行結果は以下のとおり関数の場合と同じです。

println("10 / 2 = " + g(2))
10 / 2 = 5.0

複数の引数を受け取る関数を部分適用する

複数の引数を受け取るカリー化されていない関数について見てみます。

複数の引数を受け取るカリー化されていない関数の場合は、省略する引数にアンダースコア(_)を指定することで部分適用することができます。

val nonCurriedDivFunc = (a: Float, b: Float) => a / b val g = nonCurriedDivFunc(10, _)

関数「g」に残りの引数を適用します。

println("10 / 2 = " + g(2))

結果は以下の通り部分適用した際の引数の値を関数「g」の引数の値で割った値になります。

10 / 2 = 5.0

以下のように最初の引数にアンダースコア(_)を指定することもできます。

val g = nonCurriedDivFunc(_, 10) println("2 / 10 = " + g(2))

この場合は以下の通り関数「g」の引数の値を部分適用した際の引数の値で割った値になります。

2 / 10 = 0.2

また、curriedメソッドでカリー化した関数を部分適用しても同じ結果になります。

val g = nonCurriedDivFunc.curried(10) println("10 / 2 = " + g(2))
2 / 10 = 0.2

メソッドの場合も同様にアンダースコアを指定して部分適用します。

def nonCurriedDivMethod(a: Float, b: Float) = a / b

2つめの引数にアンダースコア(_)を指定した場合

val g = nonCurriedDivMethod(10, _) println("10 / 2 = " + g(2))
10 / 2 = 5.0

1つめの引数にアンダースコア(_)を指定した場合

val g = nonCurriedDivMethod(_, 10) println("2 / 10 = " + g(2))
2 / 10 = 0.2

カリー化と部分適用は混同されやすいが違う

この記事では、カリー化と部分適用について説明しました。

カリー化は複数の引数を受け取る関数を1つの引数を受け取り、残りの引数を受け取る関数を返す関数に変換することです。

部分適用は関数の一部の引数を適用して新たな関数を作成することです。

サイト内検索