1. 概要

このチュートリアルでは、Scalaがカリー化をどのようにサポートするかを見ていきます。 部分適用関数との違いと、各アプローチを使用する利点を見ていきます。

2. カリー化

カリー化は複数の引数を持つ関数を、1つの引数を取る一連の関数に変換します。 各関数は、次の引数を使用する別の関数を返します。

2.1. 関数

まず、2つの引数を持つ関数を作成し、それをカレー関数に変換しましょう。

val sum: (Int, Int) => Int = (x, y) => x + y 
val curriedSum: Int => Int => Int = x => y => x + y
curriedSum(1)(2) shouldBe 3

使用できる複数の引数を持つ特別なcurriedメソッドもあります。

val sum: (Int, Int) => Int = (x, y) => x + y
val curriedSum: Int => Int => Int = sum.curried
curriedSum(1)(2) shouldBe 3

2.2. 方法

Scalaは複数の引数リストを作成する機能を提供するので、メソッドについても同じことを達成できます。

def sum(x: Int, y: Int): Int = x + y
def curriedSum(x: Int)(y: Int): Int = x + y

複数の引数を持つメソッドをカレー関数に変換する方法もあります。

def sum(x: Int, y: Int): Int = x + y
val curriedSum: Int => Int => Int = (sum _).curried

sum _ は、複数の引数を持つメソッドを、 curried を呼び出すことができる複数の引数を持つ関数( eta-expansion と呼ばれる)に変換します。

また、 eta-expansion は、 curried 呼び出しなしで、複数の引数リストメソッドをcurried関数に変換することに注意してください。

def sum(x: Int)(y: Int): Int = x + y
val curriedSum: Int => Int => Int = sum

3. 部分適用

部分適用は、メソッドまたは関数の作成時に引数の一部を適用することにより、引数の数を減らすプロセスです。 部分適用およびcurriedSum関数を使用して、インクリメント関数を作成してみましょう。

val curriedSum: Int => Int => Int = x => y => x + y
val increment: Int => Int = curriedSum(1)

メソッドを使用して同じことを行うことができます。

def curriedSum(x: Int)(y: Int): Int = x + y
val increment: Int => Int = curriedSum(1)

4. 型推論

型推論では、一度に1つのパラメーターリストのみが考慮されます。 つまり、場合によっては、コンパイラが適切な型を導出するのを支援できるということです。

例として、指定された述語を満たす最初の要素を返すfindメソッドを作成します。

def find[A](xs: List[A], predicate: A => Boolean): Option[A] = {
  xs match {
    case Nil => None
    case head :: tail =>
      if (predicate(head)) Some(head) else find(tail, predicate)
  }
}

この方法を使用して、最初の偶数を見つけることができます。

find(List(1, 2, 3), x => x % 2 == 0)

コンパイラはxのタイプを認識できないため、上記のコードはコンパイルされません。 この問題を解決するには、タイプをInt。として定義します。

find(List(1, 2, 3), (x:Int) => x % 2 == 0) shouldBe Some(2)

これは機能しますが、述語関数を2番目の引数リストに移動することで、型推論でコンパイラーを支援できます。

def find[A](xs: List[A])(predicate: A => Boolean): Option[A] = {
  xs match {
    case Nil => None
    case head :: tail =>
      if (predicate(head)) Some(head) else find(tail)(predicate)
  }
}

この小さな変更は、コンパイラが型を適切に解決するのに役立ちます。 Int のリストを最初の引数リストに渡すと、コンパイラは Ints を使用していることを認識し、この情報を使用して、述語のタイプがであると推測します。 Int=>ブール値。

find(List(1, 2, 3))(x => x % 2 == 0) shouldBe Some(2)

5. 柔軟性

カリー化と部分適用により、カリー化された関数にいくつかの引数を適用することにより、動作が異なる小さな関数を作成できます。 sum 関数をより一般的なものにするには、マッピング関数 f:Int => Int ID 関数とも呼ばれます)を追加します。この関数は、追加する前に両方の値をマッピングします。それらを一緒に。

def sumF(f: Int => Int)(x: Int, y: Int): Int = f(x) + f(y)

これで、 ID 関数を適用して、sum関数を取得できます。

val sum: (Int, Int) => Int = sumF(identity)
sum(1, 2) shouldBe 3

正方形を追加するsquareSum関数を作成することもできます。 これを行うには、アイデンティティ関数を置き換えます。

val sumSquare: (Int, Int) => Int = sumF(x => x * x)
sumSquare(1, 2) shouldBe 5

さらに、sum関数に基づいてincrementおよびdecrement関数を作成することもできます。

val increment: Int => Int = sum.curried(1)
val decrement: Int => Int = sum.curried(-1)
increment(2) shouldBe 3
decrement(2) shouldBe 1

カレー関数は、1つの引数関数が必要な場合に役立ちます。

たとえば、数値のリストがあり、sum関数を使用してそれぞれをインクリメントしたい状況を考えてみましょう。

val sum: (Int, Int) => Int = (x, y) => x + y
val numbers: List[Int] = List(1, 2, 3)
numbers.map(n => sum(1, n)) shouldBe List(2, 3, 4)

このコードを機能させるには、2番目の引数としてnsum関数に明示的に渡す必要があります。 これを回避し、コードの可読性を向上させるために、カリー化を使用できます。

val curriedSum: Int => Int => Int = x => y => x + y
val numbers: List[Int] = List(1, 2, 3)
numbers.map(curriedSum(1)) shouldBe List(2, 3, 4)

6. 結論

この短いチュートリアルでは、カリー化部分適用とそれらの違いについて学びました。

次に、これらがどのように連携してコードをより柔軟で読みやすくし、コンパイラが型を推測できるようにするかを確認しました。

いつものように、記事の完全なソースコードは、GitHubから入手できます。