Kotlinの高階関数について

採用はこちら

Kotlinを学び始めると、ほぼ確実に登場するのが高階関数(Higher-Order Function)という概念です。

mapfilterforEach など、普段何気なく使っている関数の多くも、実は高階関数に分類されます。

本記事では、Kotlinの高階関数について

  • 基本概念
  • 文法と仕組み
  • 実務で頻出する使い方
  • Kotlin特有の注意点

までを体系的に解説します。

目次

高階関数とは何か

高階関数とは、次のいずれかを満たす関数のことです。

  • 関数を引数として受け取る
  • 関数を戻り値として返す

Kotlinでは関数が「第一級オブジェクト」として扱えるため、変数に代入したり、引数に渡したり、戻り値として返したりできます。

この性質が、高階関数を自然に扱える理由です。

Kotlinにおける関数型(Function Type)

Kotlinでは、関数の型を次のように表現します。

(Int, Int) -> Int

これは「Int を2つ受け取り、Int を返す関数」を意味します。

関数を変数に代入する例

val add: (Int, Int) -> Int = { a, b -> a + b }
  • { a, b -> a + b } はラムダ式
  • add には「関数そのもの」が代入されます

関数を引数として受け取る高階関数

高階関数の最も基本的な形が「関数を引数として受け取る」パターンです。

fun calculate(
    a: Int,
    b: Int,
    operation: (Int, Int) -> Int
): Int {
    return operation(a, b)
}

呼び出し側

val result = calculate(10, 5) { x, y -> x + y }

このコードでは、

  • calculate が関数を引数として受け取る
  • ラムダ式が operation に渡される
  • 実行時に operation(a, b) が呼び出される

という流れになります。

Kotlinらしい書き方:トレーリングラムダ

Kotlinでは、最後の引数が関数型の場合、ラムダ式を丸括弧の外に書けます。

calculate(10, 5) {
    x, y -> x + y
}

この記法により、

  • 可読性が高い
  • DSLのような書き方が可能

というメリットが生まれます。

標準ライブラリに溢れる高階関数

Kotlinでは、日常的に高階関数を使っています。

map

val list = listOf(1, 2, 3)
val doubled = list.map { it * 2 }

filter

val even = list.filter { it % 2 == 0 }

forEach

list.forEach { println(it) }

これらはすべて「処理内容を関数として受け取る」高階関数です。

関数を戻り値として返す高階関数

高階関数は、関数を生成して返すこともできます。

fun createMultiplier(factor: Int): (Int) -> Int {
    return { value -> value * factor }
}

使用例

val triple = createMultiplier(3)
println(triple(10)) // 30

ここでは、戻り値のラムダが factor を保持しています。

クロージャ(Closure)

Kotlinのラムダは、外側の変数をキャプチャできます。

var count = 0

val increment = {
    count++
}

このように、

  • ラムダは外側の変数にアクセス可能
  • 値をコピーするのではなく、変数への参照を保持する

という点が重要です。

特に var の場合、内部的にラップされることがあり、パフォーマンス面の考慮が必要になるケースもあります。

レシーバ付き関数型(Kotlin特有の概念)

Kotlinには レシーバ付き関数型 という独自の仕組みがあります。

String.() -> Int

これは「String をレシーバとして扱い、Int を返す関数」を表します。

通常の関数型との違い

val length1: (String) -> Int = { it.length }
val length2: String.() -> Int = { length }

呼び出し例

println(length1("Hello"))
println(length2("Hello"))
println("Hello".run(length2))

String.() -> Int拡張関数とは別物であり、変数に代入された場合は invoke 経由で呼び出されます。

DSLを支える高階関数

Kotlin DSLが読みやすい理由は、高階関数 + レシーバ付き関数型 の組み合わせにあります。

fun buildString(block: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.block()
    return sb.toString()
}

使用例

val result = buildString {
    append("Hello")
    append(" ")
    append("World")
}

このような構造により、宣言的で直感的なコードが実現します。

inline関数とパフォーマンス

高階関数は便利ですが、ラムダ生成によるオーバーヘッドが発生する場合があります。

inlineの役割

inline fun repeatAction(times: Int, action: () -> Unit) {
    for (i in 0 until times) {
        action()
    }
}
  • 呼び出し時に関数本体が展開される
  • ラムダオブジェクト生成を抑制できるケースが多い
  • ただし、コードサイズ増加とのトレードオフがある

noinline / crossinline

inline fun example(
    noinline action1: () -> Unit,
    crossinline action2: () -> Unit
) {
    // ...
}
  • noinline:インライン化しない
  • crossinline:非ローカルリターンを禁止し、別コンテキストでの実行を可能にする

非ローカルリターンに注意

fun test() {
    listOf(1, 2, 3).forEach {
        if (it == 2) return
    }
    println("ここには来ない")
}
  • forEachinline
  • return により外側の関数が終了する

意図しない挙動を避けるには、ラベル付き return を使います。

listOf(1, 2, 3).forEach {
    if (it == 2) return@forEach
}
println("ここに来る")

Javaとの違い

Javaでは関数を扱うために、

  • Function
  • Consumer
  • Predicate

といった関数型インターフェースを使います。

一方、Kotlinでは

  • 関数が自然に型として扱える
  • ラムダ構文が簡潔
  • DSLや宣言的コードに向いている

という違いがあります。

高階関数が活躍する場面

  • コレクション操作
  • 処理の共通化・差し替え
  • DSL設計
  • 非同期処理・コールバック
  • 宣言的UIや設定コード

まとめ

Kotlinの高階関数は、

  • 再利用性の高い設計
  • 可読性の高い宣言的コード
  • KotlinらしいDSL表現

を支える、非常に重要な概念です。

単なる文法機能としてではなく、設計の選択肢を広げる道具として理解すると、Kotlinの表現力を最大限に活かせるようになります。

以上、Kotlinの高階関数についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次