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を持っており、R型とT型の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)

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

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

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

関数型のインスタンス化

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

  • 関数リテラル内のコードブロックを使用する方法:

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

  • 既存の宣言への呼び出し可能参照を使用する方法:

    • トップレベル、ローカル、メンバー、または拡張関数: ::isOddString::toInt
    • トップレベル、メンバー、または拡張プロパティ: List<Int>::size
    • コンストラクタ: ::Regex

    これらには、特定のインスタンスのメンバーを指すバインドされた呼び出し可能参照が含まれます: 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")
}

拡張関数への参照で変数が初期化された場合でも、関数型はデフォルトでレシーバなしで推論されます。 これを変更するには、変数の型を明示的に指定します。

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

関数型の値は、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と見なされます)。

匿名関数をパラメータとして渡す場合は、括弧の内側に配置してください。関数を括弧の外に置くことができる短縮構文は、ラムダ式にのみ適用されます。

ラムダ式と匿名関数のもう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
}