Files
obsidian/Kotlin/Функциональный Kotlin/Функции высшего порядка.md
T

393 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)