--- 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 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 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, filter: (Int) -> Boolean = { true }, transform: (Int) -> String = { it.toString() }, ): List = 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).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 ((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 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 memoize(f: (A) -> R): (A) -> R { val cache = mutableMapOf() return { arg -> cache.getOrPut(arg) { f(arg) } } } fun main() { var calls = 0 val slowSquare = memoize { 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 = 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`, `BiFunction`, `Supplier`, `Consumer` | один синтаксис `(T) -> R` | > | `Predicate` | `(T) -> Boolean` | > | `Runnable` | `() -> Unit` | > | `IntFunction`, `ToIntFunction`, ... (40+ интерфейсов) | нет, но есть упаковка | > | `f.apply(x)` | `f(x)` | > | `f.andThen(g)` | своя `then` | > | лямбда - объект | лямбда инлайнится при `inline` | > | нет аналога | лямбда с получателем `T.() -> R` | > > **Функциональный тип вместо интерфейса.** В Java функция передаётся как реализация интерфейса, и интерфейс нужно выбрать: `Function` для одного аргумента, `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, action: (Int) -> Unit) { for (i in items) action(i) } > > fun findFirstEven(items: List): 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`. Специализаций вроде `IntUnaryOperator` в Kotlin нет, поэтому в численно-интенсивном коде `inline` важен не только из-за объекта лямбды, но и из-за упаковки. Далее: [Inline-функции](Inline-функции.md)