Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,392 @@
---
title: Функции высшего порядка
description: Функции, принимающие и возвращающие функции, композиция, каррирование и мемоизация
tags:
- kotlin
- functional
---
Функция высшего порядка - это функция, которая принимает функцию в параметре или возвращает функцию. В Kotlin функциональный тип (`(Int) -> String`) полноправный, поэтому ничего особенного для этого не нужно.
```kotlin
fun applyTwice(x: Int, f: (Int) -> Int): Int = f(f(x))
fun main() {
println(applyTwice(3) { it * 2 }) // 12
println(applyTwice(3) { it + 10 }) // 23
}
```
Возврат функции:
```kotlin
fun multiplier(factor: Int): (Int) -> Int = { it * factor }
fun main() {
val triple = multiplier(3)
println(triple(5)) // 15
println(multiplier(10)(7)) // 70
}
```
`multiplier(3)` возвращает лямбду, которая **захватила** `factor`. Это замыкание, и оно работает так же, как в любом функциональном языке. Подробнее о синтаксисе: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
## Зачем это нужно
Функция высшего порядка позволяет вынести **изменяемую часть** алгоритма в параметр. Классический пример - повторяющаяся обвязка:
```kotlin
fun <T> retry(times: Int, block: () -> T): T {
var lastError: Throwable? = null
repeat(times) { attempt ->
try {
return block()
} catch (e: Exception) {
lastError = e
println("попытка ${attempt + 1} не удалась")
}
}
throw IllegalStateException("все $times попыток провалились", lastError)
}
fun main() {
var counter = 0
val result = retry(3) {
counter++
if (counter < 3) error("ещё не готово")
"успех на попытке $counter"
}
println(result) // успех на попытке 3
}
```
Логика повторов написана один раз; что именно повторять - решает вызывающий. Без функциональных типов пришлось бы заводить интерфейс с одним методом и анонимный класс.
Та же идея для измерения, транзакций и логирования:
```kotlin
inline fun <T> measured(name: String, block: () -> T): T {
val start = System.nanoTime()
try {
return block()
} finally {
println("$name: ${(System.nanoTime() - start) / 1_000} мкс")
}
}
fun main() {
val sum = measured("сумма") { (1..100_000).sumOf { it.toLong() } }
println(sum) // 5000050000
}
```
`finally` здесь важен: время выводится и при исключении. `inline` убирает объект лямбды - подробнее: [Inline-функции](Inline-функции.md)
## Функциональные типы как параметры
```kotlin
fun processNumbers(
numbers: List<Int>,
filter: (Int) -> Boolean = { true },
transform: (Int) -> String = { it.toString() },
): List<String> = numbers.filter(filter).map(transform)
fun main() {
println(processNumbers(listOf(1, 2, 3))) // [1, 2, 3]
println(processNumbers(listOf(1, 2, 3), filter = { it > 1 })) // [2, 3]
println(processNumbers(listOf(1, 2), transform = { "№$it" })) // [№1, №2]
}
```
Функциональный параметр может иметь значение по умолчанию - это заменяет перегрузки.
Nullable функциональный тип требует скобок:
```kotlin
fun handle(value: Int, onSuccess: ((Int) -> Unit)? = null): Int {
onSuccess?.invoke(value)
return value
}
fun main() {
println(handle(1)) // 1
println(handle(2) { println("получено $it") }) // получено 2 \n 2
}
```
`(Int) -> Unit?` - это функция, возвращающая `Unit?`. Скобки меняют смысл принципиально.
## Композиция
```kotlin
infix fun <A, B, C> ((A) -> B).then(next: (B) -> C): (A) -> C = { next(this(it)) }
fun main() {
val parse: (String) -> Int = { it.trim().toInt() }
val double: (Int) -> Int = { it * 2 }
val format: (Int) -> String = { "результат: $it" }
val pipeline = parse then double then format
println(pipeline(" 21 ")) // результат: 42
}
```
В stdlib готовой композиции нет - её пишут сами в три строки. Направление `then` (слева направо) читается лучше математического `compose`, но встречаются оба:
```kotlin
infix fun <A, B, C> ((B) -> C).compose(before: (A) -> B): (A) -> C = { this(before(it)) }
fun main() {
val inc: (Int) -> Int = { it + 1 }
val double: (Int) -> Int = { it * 2 }
println((inc compose double)(5)) // 11 — сначала double, потом inc
println((double compose inc)(5)) // 12 — наоборот
}
```
## Каррирование и частичное применение
```kotlin
fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C = { a -> { b -> f(a, b) } }
fun main() {
val sum: (Int, Int) -> Int = { a, b -> a + b }
val curried = curry(sum)
println(curried(2)(3)) // 5
val add10 = curried(10)
println(add10(5)) // 15
println(listOf(1, 2, 3).map(add10)) // [11, 12, 13]
}
```
На практике частичное применение чаще делают прямо лямбдой - это короче и понятнее:
```kotlin
fun log(level: String, module: String, message: String) = "[$level][$module] $message"
fun main() {
val errorInAuth: (String) -> String = { log("ERROR", "auth", it) }
println(errorInAuth("неверный пароль")) // [ERROR][auth] неверный пароль
}
```
Ещё проще - значения по умолчанию и именованные аргументы, которые в Kotlin закрывают большинство задач, решаемых каррированием в других языках. Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
## Мемоизация
```kotlin
fun <A, R> memoize(f: (A) -> R): (A) -> R {
val cache = mutableMapOf<A, R>()
return { arg -> cache.getOrPut(arg) { f(arg) } }
}
fun main() {
var calls = 0
val slowSquare = memoize<Int, Int> { calls++; it * it }
println(slowSquare(4)) // 16
println(slowSquare(4)) // 16
println(slowSquare(5)) // 25
println("вызовов: $calls") // вызовов: 2
}
```
`getOrPut` делает это в одну строку. Ограничения такого кеша: он не ограничен по размеру и не потокобезопасен - для продакшена нужен `ConcurrentHashMap` или готовый кеш.
Рекурсия с мемоизацией требует ссылки на саму функцию:
```kotlin
fun main() {
val cache = mutableMapOf(0 to 0L, 1 to 1L)
fun fib(n: Int): Long = cache.getOrPut(n) { fib(n - 1) + fib(n - 2) }
println(fib(50)) // 12586269025
println(cache.size) // 51
}
```
Локальная функция может вызывать себя, а лямбда - нет: внутри `val f = { ... f(...) }` имя `f` ещё не инициализировано.
## Функции, возвращающие функции
Типичный случай - настраиваемый валидатор:
```kotlin
fun lengthBetween(min: Int, max: Int): (String) -> String? = { value ->
when {
value.length < min -> "короче $min символов"
value.length > max -> "длиннее $max символов"
else -> null
}
}
fun validateAll(value: String, vararg rules: (String) -> String?): List<String> =
rules.mapNotNull { it(value) }
fun main() {
val notBlank: (String) -> String? = { if (it.isBlank()) "пустое значение" else null }
println(validateAll("ab", notBlank, lengthBetween(3, 10))) // [короче 3 символов]
println(validateAll("привет", notBlank, lengthBetween(3, 10))) // []
println(validateAll(" ", notBlank)) // [пустое значение]
}
```
Правила - обычные значения: их можно хранить в списке, передавать и комбинировать. Это основная практическая выгода функций высшего порядка.
## Ссылки на функции вместо лямбд
```kotlin
fun isEven(x: Int) = x % 2 == 0
fun main() {
val numbers = listOf(1, 2, 3, 4)
println(numbers.filter { isEven(it) }) // [2, 4] — лямбда-обёртка
println(numbers.filter(::isEven)) // [2, 4] — ссылка напрямую
println(numbers.map(Int::toString)) // [1, 2, 3, 4] — ссылка на метод
println(listOf("1", "2").map(String::toInt)) // [1, 2]
val strings = listOf("bb", "a", "ccc")
println(strings.sortedBy(String::length)) // [a, bb, ccc]
}
```
Ссылка `::isEven` короче и не создаёт лишнего уровня вложенности. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
## Лямбда с получателем
Особый вид функционального типа - `T.() -> R`. Внутри такой лямбды `this` - это `T`:
```kotlin
fun buildMessage(build: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.build()
return sb.toString()
}
fun main() {
val text = buildMessage {
append("Привет") // this == StringBuilder
append(", ")
append("мир")
}
println(text) // Привет, мир
}
```
На этом построены все билдеры Kotlin, включая `buildList` и `apply`. Подробнее: [Receivers и context parameters](Receivers%20и%20context%20parameters.md), [DSL](DSL.md)
## Карта раздела
| Заметка | О чём |
|---|---|
| [Inline-функции](Inline-функции.md) | как убрать накладные расходы лямбд |
| [Функции-расширения](Функции-расширения.md) | добавление методов к готовым типам |
| [Scope-функции](Scope-функции.md) | `let`, `run`, `apply`, `also`, `with` |
| [Инфиксные и операторные функции](Инфиксные%20и%20операторные%20функции.md) | свой синтаксис для своих типов |
| [Receivers и context parameters](Receivers%20и%20context%20parameters.md) | неявный `this` и контекстные зависимости |
| [DSL](DSL.md) | сборка всего перечисленного в язык под задачу |
| [Паттерны проектирования](Паттерны%20проектирования.md) | что от паттернов осталось в Kotlin |
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Function<T, R>`, `BiFunction`, `Supplier`, `Consumer` | один синтаксис `(T) -> R` |
> | `Predicate<T>` | `(T) -> Boolean` |
> | `Runnable` | `() -> Unit` |
> | `IntFunction`, `ToIntFunction`, ... (40+ интерфейсов) | нет, но есть упаковка |
> | `f.apply(x)` | `f(x)` |
> | `f.andThen(g)` | своя `then` |
> | лямбда - объект | лямбда инлайнится при `inline` |
> | нет аналога | лямбда с получателем `T.() -> R` |
>
> **Функциональный тип вместо интерфейса.** В Java функция передаётся как реализация интерфейса, и интерфейс нужно выбрать: `Function<T,R>` для одного аргумента, `BiFunction` для двух, `Supplier` без аргументов, `Consumer` без результата, `Predicate` для `boolean`, плюс примитивные специализации - в `java.util.function` их больше сорока. В Kotlin тип записывается напрямую:
> ```kotlin
> fun demo(
> supplier: () -> Int,
> consumer: (Int) -> Unit,
> predicate: (Int) -> Boolean,
> biFunction: (Int, Int) -> Int,
> ) {
> val v = supplier()
> consumer(v)
> println("${predicate(v)} ${biFunction(v, v)}")
> }
>
> fun main() { demo({ 2 }, { }, { it > 1 }, { a, b -> a + b }) } // true 4
> ```
> Под капотом это всё равно интерфейсы `Function0`, `Function1`, ... `Function22`, но в коде их не видно. Три с лишним аргумента в Java требуют собственного интерфейса - в Kotlin предел 22.
>
> **Вызов через `invoke`, а не `apply`.** В Java функциональный объект вызывают методом интерфейса: `f.apply(x)`, `p.test(x)`, `c.accept(x)`, `s.get()` - имя зависит от интерфейса. В Kotlin оператор `()` работает для любого функционального типа, потому что все они объявляют `operator fun invoke`.
>
> **Композиции в stdlib нет.** Java даёт `Function.andThen`, `Function.compose`, `Predicate.and/or/negate`. Kotlin их не предоставляет сознательно: обычно короче написать одну лямбду, чем собирать её из частей. Когда композиция действительно нужна, она пишется в одну строку - как `then` выше.
>
> **Лямбда с получателем аналога в Java не имеет.** Это основа билдеров и DSL в Kotlin; в Java эквивалента нет даже близко, поэтому билдеры там пишут методами, возвращающими `this`.
>
> Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md), [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md)
> [!warning] Подводные камни
> **Лямбда без `inline` создаёт объект на каждый вызов:**
> ```kotlin
> fun notInlined(block: () -> Unit) { block() }
> inline fun inlined(block: () -> Unit) { block() }
>
> fun main() {
> repeat(3) { notInlined { } } // объект Function0 на каждый вызов
> repeat(3) { inlined { } } // объектов нет
> println("готово")
> }
> ```
> Лямбда без захвата переменных кешируется компилятором в синглтон, а лямбда с захватом - нет. В горячем цикле это заметно. Подробнее: [Inline-функции](Inline-функции.md)
>
> **`return` внутри лямбды возвращает из внешней функции - но только в inline:**
> ```kotlin
> inline fun forEachInline(items: List<Int>, action: (Int) -> Unit) { for (i in items) action(i) }
>
> fun findFirstEven(items: List<Int>): Int? {
> forEachInline(items) { if (it % 2 == 0) return it } // выход из findFirstEven
> return null
> }
>
> fun main() { println(findFirstEven(listOf(1, 3, 4, 6))) } // 4
> ```
> В не-inline функции такой `return` не скомпилируется - нужен `return@label`. Это разное поведение для внешне одинакового кода.
>
> **Nullable функциональный тип требует скобок:**
> ```kotlin
> fun main() {
> val a: ((Int) -> Unit)? = null // функция может быть null
> val b: (Int) -> Unit? = { } // функция возвращает Unit?
> println("${a == null} ${b(1)}") // true kotlin.Unit
> }
> ```
> Без скобок получается совсем другой тип, и ошибка проявится далеко от объявления.
>
> **Вызов nullable-функции только через `invoke`:**
> ```kotlin
> fun main() {
> val f: ((Int) -> Int)? = { it * 2 }
> // println(f?(1)) // синтаксическая ошибка
> println(f?.invoke(1)) // 2
> }
> ```
>
> **Захваченная `var` меняется после создания лямбды:**
> ```kotlin
> fun main() {
> var counter = 0
> val show = { println("counter = $counter") }
> counter = 10
> show() // counter = 10, а не 0
> }
> ```
> В отличие от Java, Kotlin разрешает захват изменяемых переменных - они оборачиваются в объект-ссылку. Удобно, но лямбда перестаёт быть чистой функцией и становится зависимой от момента вызова.
>
> **Лямбды в цикле захватывают переменную, а не значение:**
> ```kotlin
> fun main() {
> val actions = mutableListOf<() -> Int>()
> var i = 0
> while (i < 3) { actions.add { i }; i++ }
> println(actions.map { it() }) // [3, 3, 3] — все видят финальное i
>
> val correct = (0 until 3).map { v -> { v } }
> println(correct.map { it() }) // [0, 1, 2] — параметр цикла свой у каждой
>
> val fixed = mutableListOf<() -> Int>()
> for (j in 0 until 3) fixed.add { j }
> println(fixed.map { it() }) // [0, 1, 2] — переменная for неизменяема
> }
> ```
>
> **Мемоизация течёт по памяти.** `mutableMapOf` в замыкании растёт без ограничений и держит все аргументы и результаты. Если функция живёт всё время работы приложения, это утечка; нужен кеш с ограничением размера.
>
> **Мемоизация не потокобезопасна.** `getOrPut` на обычном `MutableMap` при одновременном доступе может повредить структуру. Минимальная защита - `ConcurrentHashMap`, но и у него `getOrPut` не атомарен: используйте `computeIfAbsent`.
>
> **Лямбда не может ссылаться на себя:**
> ```kotlin
> fun main() {
> // val fact: (Int) -> Int = { if (it <= 1) 1 else it * fact(it - 1) } // ошибка
> fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) // локальная функция может
> println(fact(5)) // 120
> }
> ```
>
> **Больше двух-трёх функциональных параметров делают вызов нечитаемым.** `process(list, { }, { }, { })` ничего не говорит о смысле аргументов. Варианты: именованные аргументы, объект-конфигурация или интерфейс с несколькими методами.
>
> **Функциональные типы упаковывают примитивы.** `(Int) -> Int` в байт-коде - это `Function1<Integer, Integer>`. Специализаций вроде `IntUnaryOperator` в Kotlin нет, поэтому в численно-интенсивном коде `inline` важен не только из-за объекта лямбды, но и из-за упаковки.
Далее: [Inline-функции](Inline-функции.md)