Введение: функция, которая принимает функцию

«Что такое функция высшего порядка?» — вопрос, который звучит просто, а на деле проверяет, понимаете ли вы 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: функция высшего порядка, которая принимает начальное значение и комбинирующую функцию:

Fold.kt
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). Никакого рефлекшена и обёрток.

Определение

Функция высшего порядка — функция, которая принимает функции как параметры или возвращает функцию. Всё остальное — детали синтаксиса и типов.

Способы передать функцию

У функции-значения есть несколько способов появиться в аргументах:

  1. Лямбда — { acc, i -> acc + i } (самый частый).
  2. Ссылка на функцию — ::isEven, Int::times, String::toInt.
  3. Анонимная функция — fun(x: Int): Boolean = x > 0.
  4. Объект класса, реализующего функциональный тип — через operator fun invoke.

Ссылки на функции недооценены кандидатами, а зря — это чистый способ переиспользовать существующую функцию без обёрток:

CallableReference.kt
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):

NullableCallback.kt
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)? — скобки вокруг всего типа обязательны.

Функции, которые возвращают функции

Функция высшего порядка может не только принимать функции, но и возвращать их. Это удобно, когда нужно сконфигурировать поведение:

Factory.kt
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:

Receiver.kt
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: тело функции вместе с лямбдой «разворачивается» в место вызова, объект функции не создаётся.

InlineFun.kt
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 работает:

NonLocalReturn.kt
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, разберём вашу ситуацию и составим план, что закрыть в первую очередь.