[Kotlin] invoke演算子と多重定義、ラムダ式、関数オブジェクト

Kotlinにおいて、invoke演算子 (invoke operator﻿) とは、**関数オブジェクト**の本体部分です。
Kotlinプログラマーが扱う関数オブジェクトは大抵ラムダ式で表されていて、invoke演算子は関数オブジェクトを引数にとる関数を書いたときに使う以外ほとんどお目にかからないと思いますが、本稿では他の記事との絡みもあって、演算子多重定義などのニッチな話題を書いてみます。

まずはとりあえず、関数オブジェクトとラムダ式について、馴染みのない方向けに説明します。今回のコードを検証したKotlinのバージョンは1.5.31です。

## 関数オブジェクトとラムダ式

「ラムダ式」とは、簡潔な書式で記述された「関数オブジェクト」です。「関数オブジェクト」は、その名の通り関数の機能を持つオブジェクトです。すでにラムダ式に慣れている方は、読み飛ばして2つ先のinvoke演算子に行っていただいて結構です。

以下のコードにて `{` から `}` までの部分がラムダ式です。このように他の関数の引数として記述されるのが一般的です。

```kotlin
printResults(5.0, 3.0) { n, x, y -> (x.pow(n) + y.pow(n)).pow(1 / n.toDouble()) }
```

ラムダ式を引数として受け取る側の関数は、たとえば以下のように書かれます。

```kotlin
fun printResults(xMax: Double, yMax: Double, norm: (Int, Double, Double) -> Double) {
    val y = 0.1
    while (y < yMax) {
        val x = 0.1
        while (x < xMax) {
            println("norm($x, $y) = ${norm(2, x, y)}")
        }
    }
}
```

関数 `printResults` は3つの引数を受け取ります。ここで、ラムダ式も引数なので、

```kotlin
printResults(5.0, 3.0, { n, x, y -> (x.pow(n) + y.pow(n)).pow(1 / n.toDouble()) })
```

と `()` の中に収めなければならないように思われますが、Kotlinでは**関数の最後の引数がラムダ式だった場合に限って `()` の外に記述する**ことが文法上認められています。そして、最後の引数のラムダ式はむしろ `()` の外に記述することが推奨されています。また、関数の引数がラムダ式1つだけの場合、以下のように `()` を省略した記法が可能です。

```kotlin
setListener { number -> print(number) } // { print(it) }
(0..5).forEach { print(it) } // 'argument ->' omitted
```

ラムダ式の中の引数が1つのとき、 `argument ->` の部分を省略することができ、その場合には `it` が引数変数となります。

上のコードの `forEach` は組み込み関数で、 `Iterable` 型のインスタンスをreceiverとする**拡張メンバ関数**として定義されています。Kotlinには、 `forEach` のような組み込み関数が多数用意されています。

## ラムダ式の引数の型

ラムダ式を引数として受け取る側の関数において、その引数が

```kotlin
norm: (Int, Double, Double) -> Double
```

と定義されていることにより、ラムダ式の引数の型が `(Int, Double, Double)` であること、戻り値の型が `Double` であることが決まります。この組み合わせにより、ラムダ式は以下で表せる関数を関数オブジェクト化したのと同じものになります。

```kotlin
fun anonymousFunction(n: Int, x: Double, y: Double) {
    return (x.pow(n) + y.pow(n)).pow(1 / n.toDouble())
}
```

引数として渡すだけでなく、以下のように書いて関数オブジェクトをインスタンス化して使うこともできます。

```kotlin
val functionObject: (Int, Double, Double) -> Double =
    { n, x, y -> (x.pow(n) + y.pow(n)).pow(1 / n.toDouble()) })
val distance = functionObject(2, 1.5, 2.5)
```

ただし、以下のように書くことはできません。この書き方ではラムダ式の引数の型が決まらないためです。

```kotlin
{ n, x, y -> (x.pow(n) + y.pow(n)).pow(1 / n.toDouble()) }(2, 1.5, 2.5) // compile error!!
```

以下のようにしてラムダ式の引数の型を指定することは可能ですが、

```kotlin
val distance = { n: Int, x: Double, y: Double ->
    (x.pow(n) + y.pow(n)).pow(1 / n.toDouble()) }(2, 1.5, 2.5) // OK! But...
```

ラムダ式を即値として使うよりも以下のように書くほうが簡単なので、通常は即値としてラムダ式を使う必要はなく、ラムダ式内で引数の型の指定が必須になることもあまりないでしょう。

```kotlin
val distance = (1.5.pow(2) + 2.5.pow(2)).pow(1 / 2.0)
```

## invoke演算子

ラムダ式で作った関数オブジェクトは、以下のように使えます（再掲）。

```kotlin
val functionObject: (Int, Double, Double) ->
    Double = { n, x, y -> (x.pow(n) + y.pow(n)).pow(1 / n.toDouble()) })
val distance = functionObject(2, 1.5, 2.5)
```

しかし「関数オブジェクト」といえども `functionObject` はインスタンスなのになぜ関数っぽく `functionObject(2, 1.5, 2.5)` と書けるのでしょうか？ それは、引数列を囲っている `()` が、独自の計算をする**演算子**として暗黙のうちに定義されているからです。

`()` 演算子は、以下のようにして `invoke` という別名を使って、名前のあるクラスに定義を与えることもできます。ラムダ式は、invoke演算子が以下のように定義されたクラスを無名化したもの、ともいえます。

```kotlin
public operator fun invoke(n: Int, x: Double, y: Double): Double {
    return (x.pow(n) + y.pow(n)).pow(1 / n.toDouble())
}
```

## 演算子の多重定義

invoke演算子と似た例としてget演算子があります。 `List`, `Map` などのKotlinの組み込みコレクションクラスが

```kotlin
val list = listOf("Rock", "Paper", "Scissors")
val listItem = list[1]
val map = mapOf("one" to 1, "two" to 2, "three", to 3)
val mappedValue = map["two"]
```

と `[]` でくくった引数で指し示された値を返すことができるのは、以下のような書式でget演算子が定義されているためです（ `get` は `[]` の別名）。

```kotlin:Collection.kt
// List<out E> // E is a generic type of List
public operator fun get(index: Int): E
```

invoke演算子やget演算子の定義は、対象のクラス各々に使われる `()` や `[]` の演算子にそれぞれ個別の定義を与えています。これを**演算子の多重定義** (Operator overloading) と呼びます。

`()` 演算子や `[]` 演算子は、以下のように別名を使って記述することもできます。

```kotlin
val distance = functionObject.invoke(2, 1.5, 2.5)

val list = listOf("Rock", "Paper", "Scissors")
val listItem = list.get(1)
val map = mapOf("one" to 1, "two" to 2, "three", to 3)
val mappedValue = map.get("two")
```

Kotlinにおいて、関数の機能を持つオブジェクト、という関数オブジェクトの定義をより具体的に表すと、「invoke演算子が実装されているオブジェクト」ということになります。invoke演算子の実装法には、ラムダ式を使って暗黙的に実装する方法と、 `()` 演算子を多重定義する書式を使って明示的に実装する方法の2通りがある、ということです。

## 名前つき関数オブジェクトクラス

上のコードで示した、ラムダ式を引数として受け取る側の関数（再掲）は、以下のように、`(Int, Double, Double) -> Double` という型の関数オブジェクトを引数で受け取ります。

```kotlin
fun printResults(xMax: Double, yMax: Double, norm: (Int, Double, Double) -> Double) {
    // ...
}
```

これは、 `norm` が演算子 `invoke(Int, Double, Double) -> Double` の実装を備えていることを示しています。

そして、invoke演算子はラムダ式でなくても、名前のあるクラスや、スーパークラスを継承した無名クラスに前述の演算子多重定義構文を使って実装できます。これを使って、大抵のインスタンスは関数オブジェクトになる（関数オブジェクトの機能を兼ねる）ことができます。

```kotlin:FunctionClass.kt
class FunctionClass(private val n: Int) {
    operator fun invoke(x: Double, y: Double): Double =
        (x.pow(n) + y.pow(n)).pow(1 / n.toDouble())
}
```

以下のように使えます。

```kotlin
val functionObject = FunctionClass(2)
val distance = functionObject(1.5, 2.5)
```

ただし、この関数オブジェクトを他の関数の呼び出しに使おうとするときには注意が必要です。
ラムダ式を引数として受け取る側の関数

```kotlin
fun printResults(xMax: Double, yMax: Double, norm: (Double, Double) -> Double) {
    // ...
}
```

これを以下のように呼び出そうとすると、コンパイルエラーになります。

```kotlin
val printResults(5.0, 3.0, functionObject)
```

`FunctionClass` を直接使っている場合には `operator fun invoke(x: Double, y: Double): Double` にアクセスできても、関数の引数に使うと、この場合には `FunctionClass` が `(Double, Double) -> Double` 型でないとみなされ、コンパイルできません。

この用途で使えるようにするには、 `FunctionClass` が `(Double, Double) -> Double` 型であることを定義で明示する必要があります。

```kotlin:FunctionClass.kt
class FunctionClass(private val n: Int) : (Double, Double) -> Double {
    override fun invoke(x: Double, y: Double): Double =
        (x.pow(n) + y.pow(n)).pow(1 / n.toDouble())
}
```

`(Double, Double) -> Double` を継承していることから、 `operator fun invoke(x: Double, y: Double): Double` の定義がすでに存在しているため、 `operator fun invoke(x: Double, y: Double): Double` の実装に override 修飾子が必要になっています。その代わり、 `override operator fun` で演算子多重定義を記述するときは、 operator を省略して `override fun` と書けます。

このようにして既存クラスに関数オブジェクトの機能を兼ねさせることもできますし、以下のように拡張関数、もとい拡張演算子を既存のクラス（finalクラスであっても）に付け加えることもできます。

```kotlin
operator fun String.invoke(): String {
    return "$this.invoke()"
}

val invokedString = "String"()()()
```

## コーディングルールとinvoke演算子

関数オブジェクト `functionObject` を使うとき、以下ではインスタンスでなく関数と見間違えるかもしれないし、

```kotlin
functionObject(1, '2', "3")
```

以下だと今度は普通のインスタンス関数みたい。

```kotlin
functionObject.invoke(1, '2', "3")
```

コーディングルールを厳格に定めることに慣れている開発者や開発チームは、面倒でも関数オブジェクトの呼び出し書式もあらかじめ決めておくと無難かもしれません。invoke演算子の主旨を考えると上の文法を使うべきかもしれませんが、上下を適宜使い分けてもよい、と筆者は思います。ただし、使い分け方は明確になっているほうがよいでしょう。

## 演算子多重定義の文法

以下のように、複数の関数オブジェクト型をクラスに与えることは（現状では）できないようです。

```kotlin
class FunctionClass(private val n: Int) : (Double, Double) -> Double, () -> String {
    // compile error!!
}
```

複数の型を与えなければ、以下のように引数の異なる複数のinvoke演算子を定義することは可能のようです

```kotlin
class FunctionClass(private val n: Int) : (Double, Double) -> Double {
    override fun invoke(x: Double, y: Double): Double =
        (x.pow(n) + y.pow(n)).pow(1 / n.toDouble())

    operator fun invoke(): String = "$this.invoke()"
}
```

このあたりの文法はかなり複雑のようで、ちょっとした変化でコンパイルできたりできなかったりと変わります。複数の関数定義をもつ関数オブジェクト、なるものが欲しくなるケースは滅多にないでしょうし、本稿でもこれ以上の深入りは避けます。きわどい書き方もほどほどに。最後は重箱の隅つつきになってしまいました。

## 偽コンストラクタ？

正直なところ、筆者も `()` 演算子の多重定義を使いたくなるケースは限られていると思います。

しかし最近になって、[別のテーマ](https://qiita.com/yamadacsa/items/f7bf7acee968274913cf)で記事を書いていて、演算子多重定義の思いもよらない使い方があることを知りまして、しかもそのテーマの記事を書いていたらとてつもなく長大になってしまったため、invoke演算子の記事をこちらに切り出しました。本稿だけですでに長いですね(汗)。ここまでお付き合いいただき、ありがとうございました。

## 関連記事

- [[Kotlin] 「偽コンストラクタ」の作り方 その1](https://qiita.com/yamadacsa/items/f7bf7acee968274913cf)
- [[Kotlin] 「偽コンストラクタ」の作り方 その2](https://qiita.com/yamadacsa/items/fb92e28ac46d7901bb0b)

## 参考文献

- [Operator overloading | Kotlin](https://kotlinlang.org/docs/operator-overloading.html)
- [Kotlinのinvoke operator | by Kenji Abe | Medium](https://star-zero.medium.com/kotlinのinvoke-operator-5fe733e75738)
- Kotlinの kotlin.collections.Collection.kt, kotlin.collections._Collection.kt のソースコード