Skip to content

高階関数とラムダ

Kotlinの関数はファーストクラスです。これは、関数を変数やデータ構造に格納したり、他の高階関数への引数として渡したり、高階関数から返したりできることを意味します。他の非関数値に対して可能なあらゆる操作を関数に対して実行できます。

これを容易にするため、Kotlinは静的型付けプログラミング言語として、関数を表現するための関数型のファミリーを使用し、ラムダ式のような特殊な言語構造のセットを提供しています。

高階関数

高階関数とは、関数をパラメータとして取る関数、または関数を返す関数のことです。

高階関数の良い例は、コレクションに対する関数型プログラミングイディオムであるfoldです。これは、初期アキュムレータ値と結合関数を取り、現在のアキュムレータ値と各コレクション要素を連続的に結合し、毎回アキュムレータ値を置き換えることによって戻り値を構築します。

kotlin
fun <T, R> Collection<T>.fold(
    initial: R, 
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

上記のコードでは、combineパラメータは関数型 (R, T) -> R を持っており、RT型の2つの引数を取り、R型の値を返す関数を受け入れます。 これはforループ内で呼び出され、その戻り値はaccumulatorに割り当てられます。

foldを呼び出すには、関数型のインスタンスを引数として渡す必要があります。この目的には、ラムダ式(詳細は後述)が高階関数の呼び出しサイトで広く使用されます。

kotlin
fun main() {
    val items = listOf(1, 2, 3, 4, 5)
    
    // Lambdas are code blocks enclosed in curly braces.
    items.fold(0, { 
        // When a lambda has parameters, they go first, followed by '->'
        acc: Int, i: Int -> 
        print("acc = $acc, i = $i, ") 
        val result = acc + i
        println("result = $result")
        // The last expression in a lambda is considered the return value:
        result
    })
    
    // Parameter types in a lambda are optional if they can be inferred:
    val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
    
    // Function references can also be used for higher-order function calls:
    val product = items.fold(1, Int::times)
    println("joinedToString = $joinedToString")
    println("product = $product")
}

関数型

Kotlinでは、関数を扱う宣言に(Int) -> Stringのような関数型を使用します。例: val onClick: () -> Unit = ...

これらの型は、関数のシグネチャ(パラメータと戻り値)に対応する特別な表記法を持っています。

  • すべての関数型は、括弧で囲まれたパラメータ型のリストと戻り型を持っています。(A, B) -> Cは、A型とB型の2つの引数を取り、C型の値を返す関数を表す型を示します。パラメータ型のリストは() -> Aのように空にすることができます。Unitの戻り型は省略できません。

  • 関数型はオプションで、表記法のドットの前に指定される追加のレシーバ型を持つことができます。A.(B) -> C型は、レシーバオブジェクトAに対してパラメータBで呼び出すことができ、値Cを返す関数を表します。 これらの型と合わせて、レシーバを持つ関数リテラルがよく使用されます。

  • 中断関数は、suspend () -> Unitsuspend A.(B) -> Cのように、表記法にsuspend修飾子を持つ特殊な種類の関数型に属します。

関数型の表記法には、オプションで関数パラメータの名前を含めることができます。(x: Int, y: Int) -> Point。 これらの名前は、パラメータの意味を文書化するために使用できます。

関数型がnull許容型であることを指定するには、次のように括弧を使用します。((Int, Int) -> Int)?

関数型は括弧を使用して組み合わせることもできます。(Int) -> ((Int) -> Unit)

NOTE

矢印表記は右結合です。(Int) -> (Int) -> Unitは前の例と同等ですが、((Int) -> (Int)) -> Unitとは異なります。

型エイリアスを使用して、関数型に別の名前を付けることもできます。

kotlin
typealias ClickHandler = (Button, ClickEvent) -> Unit

関数型のインスタンス化

関数型のインスタンスを取得するにはいくつかの方法があります。

  • 関数リテラル内のコードブロックを使用します。以下のいずれかの形式です。

    レシーバを持つ関数リテラルは、レシーバを持つ関数型の値として使用できます。

  • 既存の宣言へのコーラブル参照を使用します。

    これらには、特定のインスタンスのメンバーを指すバウンドコーラブル参照が含まれます: foo::toString

  • インターフェースとして関数型を実装するカスタムクラスのインスタンスを使用します。

kotlin
class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

コンパイラは、十分な情報があれば変数の関数型を推論できます。

kotlin
val a = { i: Int -> i + 1 } // The inferred type is (Int) -> Int

レシーバを持つ関数型とレシーバを持たない関数型の非リテラル値は交換可能であるため、レシーバを最初のパラメータの代わりに使用したり、その逆も可能です。例えば、(A, B) -> C型の値は、A.(B) -> C型の値が期待される場所で渡したり割り当てたりできますし、その逆も可能です。

kotlin
fun main() {
    val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
    val twoParameters: (String, Int) -> String = repeatFun // OK
    
    fun runTransformation(f: (String, Int) -> String): String {
        return f("hello", 3)
    }
    val result = runTransformation(repeatFun) // OK
    println("result = $result")
}

NOTE

拡張関数への参照で変数が初期化された場合でも、関数型にレシーバがないとデフォルトで推論されます。

これを変更するには、変数型を明示的に指定します。

関数型インスタンスの呼び出し

関数型の値は、invoke(...)オペレータを使用して呼び出すことができます。f.invoke(x)または単にf(x)

値にレシーバ型がある場合、レシーバオブジェクトは最初の引数として渡されるべきです。 レシーバを持つ関数型の値を呼び出すもう一つの方法は、値が拡張関数であるかのように、レシーバオブジェクトを前置することです。1.foo(2)

例:

kotlin
fun main() {
    val stringPlus: (String, String) -> String = String::plus
    val intPlus: Int.(Int) -> Int = Int::plus
    
    println(stringPlus.invoke("<-", "->"))
    println(stringPlus("Hello, ", "world!"))
    
    println(intPlus.invoke(1, 1))
    println(intPlus(1, 2))
    println(2.intPlus(3)) // extension-like call
}

インライン関数

高階関数に対して、柔軟な制御フローを提供するインライン関数を使用すると、有益な場合があります。

ラムダ式と匿名関数

ラムダ式と匿名関数は関数リテラルです。関数リテラルとは、宣言されずに式として即座に渡される関数のことです。以下の例を考えてみましょう。

kotlin
max(strings, { a, b -> a.length < b.length })

max関数は高階関数であり、2番目の引数として関数値を取ります。この2番目の引数はそれ自体が関数であり、関数リテラルと呼ばれ、以下の名前付き関数と同等です。

kotlin
fun compare(a: String, b: String): Boolean = a.length < b.length

ラムダ式の構文

ラムダ式の完全な構文形式は次のとおりです。

kotlin
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
  • ラムダ式は常に波括弧で囲まれます。
  • 完全な構文形式のパラメータ宣言は波括弧内にあり、オプションで型アノテーションを持ちます。
  • 本体は->の後に続きます。
  • ラムダの推論された戻り型がUnitでない場合、ラムダ本体内の最後の(または唯一の)式が戻り値として扱われます。

すべてのオプションのアノテーションを省略すると、残りは次のようになります。

kotlin
val sum = { x: Int, y: Int -> x + y }

末尾ラムダの渡し方

Kotlinの規約により、関数の最後のパラメータが関数の場合、対応する引数として渡されるラムダ式は括弧の外に配置できます。

kotlin
val product = items.fold(1) { acc, e -> acc * e }

このような構文は末尾ラムダとも呼ばれます。

ラムダがその呼び出しで唯一の引数である場合、括弧を完全に省略できます。

kotlin
run { println("...") }

it: 単一パラメータの暗黙的な名前

ラムダ式が1つのパラメータしか持たないことは非常によくあります。

コンパイラがパラメータなしでシグネチャを解析できる場合、パラメータを宣言する必要はなく、->を省略できます。パラメータはitという名前で暗黙的に宣言されます。

kotlin
ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'

ラムダ式からの値の返却

ラベル付きreturn構文を使用して、ラムダから明示的に値を返すことができます。 そうでない場合、最後の式の値が暗黙的に返されます。

したがって、次の2つのスニペットは同等です。

kotlin
ints.filter {
    val shouldFilter = it > 0
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0
    return@filter shouldFilter
}

この規約は、括弧の外にラムダ式を渡すことと相まって、LINQスタイルのコードを可能にします。

kotlin
strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }

未使用の変数に対するアンダースコア

ラムダのパラメータが未使用の場合、名前の代わりにアンダースコアを配置できます。

kotlin
map.forEach { (_, value) -> println("$value!") }

ラムダにおける分解

ラムダにおける分解は、分解宣言の一部として説明されています。

匿名関数

上記のラムダ式の構文には、関数の戻り型を指定する機能が欠けています。ほとんどの場合、戻り型は自動的に推論されるため、これは不要です。しかし、明示的に指定する必要がある場合は、別の構文、つまり匿名関数を使用できます。

kotlin
fun(x: Int, y: Int): Int = x + y

匿名関数は通常の関数宣言と非常によく似ていますが、名前が省略されています。その本体は、式(上記参照)またはブロックのいずれかになります。

kotlin
fun(x: Int, y: Int): Int {
    return x + y
}

パラメータと戻り型は、通常の関数と同じ方法で指定されますが、パラメータの型はコンテキストから推論できる場合は省略できます。

kotlin
ints.filter(fun(item) = item > 0)

匿名関数の戻り型推論は、通常の関数と同じように機能します。式本体を持つ匿名関数の戻り型は自動的に推論されますが、ブロック本体を持つ匿名関数の場合は明示的に指定する必要があります(またはUnitと見なされます)。

NOTE

匿名関数をパラメータとして渡す場合は、括弧の内側に配置してください。関数を括弧の外に配置できる省略構文は、ラムダ式でのみ機能します。

ラムダ式と匿名関数のもう1つの違いは、非ローカルリターンの動作です。 ラベルのないreturnステートメントは、常にfunキーワードで宣言された関数から戻ります。これは、ラムダ式内のreturnは囲んでいる関数から戻るのに対し、匿名関数内のreturnは匿名関数自体から戻ることを意味します。

クロージャ

ラムダ式や匿名関数(およびローカル関数オブジェクト式)は、外側のスコープで宣言された変数を含むクロージャにアクセスできます。クロージャでキャプチャされた変数はラムダ内で変更できます。

kotlin
var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

レシーバを持つ関数リテラル

A.(B) -> Cのようなレシーバを持つ関数型は、関数リテラルの特殊な形式、つまりレシーバを持つ関数リテラルでインスタンス化できます。

前述の通り、Kotlinはレシーバを持つ関数型のインスタンスを呼び出す際に、レシーバオブジェクトを提供できる機能を提供しています。

関数リテラルの本体内では、呼び出しに渡されたレシーバオブジェクトは暗黙的なthisとなるため、追加の修飾子なしでそのレシーバオブジェクトのメンバーにアクセスしたり、thisを使用してレシーバオブジェクトにアクセスしたりできます。

この動作は、拡張関数の動作に似ています。拡張関数も、関数本体内でレシーバオブジェクトのメンバーにアクセスできます。

以下に、レシーバを持つ関数リテラルとその型の例を示します。ここでplusはレシーバオブジェクトで呼び出されます。

kotlin
val sum: Int.(Int) -> Int = { other -> plus(other) }

匿名関数の構文では、関数リテラルのレシーバ型を直接指定できます。 これは、レシーバを持つ関数型の変数を宣言し、後でそれを使用する必要がある場合に役立ちます。

kotlin
val sum = fun Int.(other: Int): Int = this + other

ラムダ式は、レシーバ型がコンテキストから推論できる場合に、レシーバを持つ関数リテラルとして使用できます。 その最も重要な使用例の1つは、型安全なビルダーです。

kotlin
class HTML {
    fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // create the receiver object
    html.init()        // pass the receiver object to the lambda
    return html
}

html {       // lambda with receiver begins here
    body()   // calling a method on the receiver object
}