393 lines
21 KiB
Markdown
393 lines
21 KiB
Markdown
---
|
||
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)
|