Введение: функция, которая принимает функцию
«Что такое функция высшего порядка?» — вопрос, который звучит просто, а на деле проверяет, понимаете ли вы Kotlin как язык с функциями-значениями. Половина кандидатов отвечает «это функция, которая принимает другую функцию», и на этом всё. А дальше начинается интересное: типы функций, ссылки, inline, ресиверы.
Функция высшего порядка — это функция, которая принимает функции как параметры или возвращает функцию. Именно на этой механике построены коллекции (filter, map, fold), корутины, Compose и большая часть архитектуры Android. В этой статье разберём тему по официальной документации: от типов функций до inline — так, как это спрашивают на собеседованиях.
Типы функций — фундамент темы
Прежде чем говорить о функциях высшего порядка, нужно разобраться, как Kotlin записывает «тип функции». Это нотация: параметры в скобках, результат после стрелки.
(Int, Int) -> Int— принимает дваInt, возвращаетInt.() -> Unit— без параметров, ничего не возвращает.Unitв типе опустить нельзя.((String) -> Int)?— nullable тип функции: переменная может не содержать функцию вообще.suspend () -> Unit— тип suspend-функции, основа корутин.A.(B) -> C— тип функции с ресивером: вызывается как метод объектаA.
Классический пример из документации — fold: функция высшего порядка, которая принимает начальное значение и комбинирующую функцию:
fun <T, R> Collection<T>.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R,
): R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}
fun main() {
val items = listOf(1, 2, 3, 4, 5)
println(items.fold(0) { acc, i -> acc + i }) // 15
println(items.fold(1, Int::times)) // 120
}Обратите внимание: параметр combine имеет тип (acc: R, nextElement: T) -> R — функцию можно просто вызвать как обычный метод: combine(accumulator, element). Никакого рефлекшена и обёрток.
Определение
Функция высшего порядка — функция, которая принимает функции как параметры или возвращает функцию. Всё остальное — детали синтаксиса и типов.
Способы передать функцию
У функции-значения есть несколько способов появиться в аргументах:
- Лямбда —
{ acc, i -> acc + i }(самый частый). - Ссылка на функцию —
::isEven,Int::times,String::toInt. - Анонимная функция —
fun(x: Int): Boolean = x > 0. - Объект класса, реализующего функциональный тип — через
operator fun invoke.
Ссылки на функции недооценены кандидатами, а зря — это чистый способ переиспользовать существующую функцию без обёрток:
fun isEven(x: Int): Boolean = x % 2 == 0
fun main() {
val numbers = listOf(1, 2, 3, 4)
println(numbers.filter(::isEven)) // [2, 4]
println(numbers.filter { it % 2 == 0 }) // то же самое лямбдой
}Переменная функционального типа вызывается как обычная функция — f(x) или f.invoke(x):
fun runIfNotNull(callback: ((String) -> Unit)?) {
callback?.invoke("hello")
}
fun main() {
runIfNotNull { println(it.uppercase()) } // HELLO
runIfNotNull(null) // ничего не произойдёт
}Подводный камень: fun printAll(callbacks: List<(Int) -> Unit>) — это список функций, а fun printAll(callback: (Int) -> Unit) — одна функция. Внимательно считайте скобки: nullable-тип функции записывается как ((Int) -> Int)? — скобки вокруг всего типа обязательны.
Функции, которые возвращают функции
Функция высшего порядка может не только принимать функции, но и возвращать их. Это удобно, когда нужно сконфигурировать поведение:
fun makeAdder(step: Int): (Int) -> Int = { value -> value + step }
fun main() {
val addTen = makeAdder(10)
println(addTen(5)) // 15
val addHundred = makeAdder(100)
println(addHundred(5)) // 105
}Такая «фабрика функций» — реальный паттерн в Android: фабрики колбэков, конвертеры, обработчики ошибок. На собеседовании хороший ход — привести пример из своего проекта: «у нас был маппер, который возвращал функцию форматирования в зависимости от локали».
К этому же семейству относятся функции с ресивером — A.(B) -> C. Внутри такой функции объект A становится this, и методы вызываются без квалификатора. Именно так работают apply, with, run и buildList:
fun main() {
val config = StringBuilder().apply {
append("host=")
append("example.com")
}
println(config) // host=example.com
val numbers = buildList {
add(1)
add(2)
add(3)
}
println(numbers) // [1, 2, 3]
}Внутри apply { } и buildList { } вы работаете с ресивером напрямую — append(...), add(...) — потому что лямбда имеет тип StringBuilder.() -> Unit и MutableList<Int>.() -> Unit.
inline: зачем это вообще нужно
Раз уж функции передаются как значения — возникает вопрос производительности. Объект функции в рантайме стоит памяти, а вызов через него — накладных расходов. Kotlin решает это ключевым словом inline: тело функции вместе с лямбдой «разворачивается» в место вызова, объект функции не создаётся.
inline fun measureTime(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
println("Прошло ${System.currentTimeMillis() - start} мс")
}
fun main() {
measureTime {
Thread.sleep(10)
}
}Из inline вытекает второе важное следствие — нелокальный return. Раз лямбда развернута в место вызова, return внутри неё возвращает из объемлющей функции. Поэтому в forEach или launch голый return работает:
fun main() {
listOf(1, 2, 3).forEach {
if (it == 2) return // выходит из main
println(it) // 1
}
}А если лямбду передали в не-inline функцию — такой return не скомпилируется. В документации это описано как возврат из функции, объявленной с fun. Ещё одно следствие inline — reified-типы, но это тема отдельного разговора про дженерики.
Частая ошибка на собеседовании: «inline нужен для оптимизации, поэтому я пометил inline все свои функции». На деле inline даёт выигрыш только на функциях с лямбдами, при этом раздувает байткод и запрещает некоторые вещи внутри лямбды (например, return из неё, если функция помечена crossinline). Стандартная библиотека уже inline там, где это выгодно.
Где это живёт в Android
Теперь свяжем теорию с тем, что вы пишете каждый день.
- Коллекции:
filter,map,fold,groupBy— каждая принимает лямбду. - Корутины:
launch { }— функция с последним параметромsuspend () -> Unit. - Flow:
collect { },map { },onEach { }— операторы на функциях высшего порядка. - Compose:
setContent { },remember { },LaunchedEffect { }— всё это функции с лямбдами. - ViewModel и слой данных: колбэки
onSuccess,onError, обработчики кликов.
Один механизм
Корутины, коллекции и Compose используют один и тот же механизм — функции высшего порядка. Понимая его, вы объясняете «как устроен collect» без заучивания.
Когда я на мок-собеседовании прошу написать функцию retry(times: Int, block: () -> Unit), половина кандидатов справляется. А вот на вопросе «почему в launch { return } компилируется, а в не-inline функции — нет» начинается тишина. Именно такие стыки теории и практики отделяют средний уровень от крепкого.
Готовитесь к переходу на middle?
Бесплатная диагностика: разберу ваши пробелы и составлю план роста за 30–40 минут. Без обязательств — если менторство вам не подойдёт, честно скажу.
Итоги
Функции высшего порядка — сквозная тема: она связывает язык, корутины и архитектуру.
- Определение: функция принимает функции или возвращает функцию; типы записываются как
(A, B) -> C. - Передача: лямбды, ссылки
::, анонимные функции, nullable-типы((Int) -> Int)?. - Производительность и контроль:
inlineразворачивает лямбды, даёт нелокальный return и лежит в основе стандартной библиотеки.
Главное
Лямбды, ссылки на функции и inline — три кита темы. Если вы можете объяснить collect { } как вызов функции с параметром suspend () -> Unit, тему функций высшего порядка можно считать закрытой.
Если чувствуете, что язык знаете «на уровне кода», а объяснить его не можете — это не приговор, а вопрос тренировки. Напишите в Telegram, разберём вашу ситуацию и составим план, что закрыть в первую очередь.