Spec-Zone.ru › Kotlin 1.9

Функции высшего порядка и лямбда-выражения

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

Для этого Kotlin, как статически типизированный язык программирования, использует семейство типов функций для представления функций и предоставляет набор специализированных конструкций языка, таких как лямбда-выражения.

Функции высшего порядка

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

Хорошим примером функции высшего порядка является идиома функционального программирования 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
}

В приведенном коде параметр combine имеет тип функции (R, T) -> R, поэтому он принимает функцию, которая принимает два аргумента типов R и T и возвращает значение типа R. Он вызывается внутри цикла for, а возвращаемое значение затем присваивается accumulator.

Чтобы вызвать fold, необходимо передать экземпляр типа функции в качестве аргумента, и лямбда-выражения (подробно описанные ниже) широко используются для этой цели в местах вызова функций высшего порядка:

fun main() {
    //sampleStart
    val items = listOf(1, 2, 3, 4, 5)
    
    // Lambdas are code blocks enclosed in curly braces.
    items.fold(0, { 
        // When a lambda has parameters, they go first, followed by '->'
        acc: Int, i: Int -> 
        print("acc = $acc, i = $i, ") 
        val result = acc + i
        println("result = $result")
        // The last expression in a lambda is considered the return value:
        result
    })
    
    // Parameter types in a lambda are optional if they can be inferred:
    val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
    
    // Function references can also be used for higher-order function calls:
    val product = items.fold(1, Int::times)
    //sampleEnd
    println("joinedToString = $joinedToString")
    println("product = $product")
}

Типы функций

Kotlin использует типы функций, такие как (Int) -> String, для объявлений, связанных с функциями: val onClick: () -> Unit = ....

Эти типы имеют специальную запись, которая соответствует сигнатурам функций — их параметрам и возвращаемым значениям:

  • Все типы функций имеют заключенный в скобки список типов параметров и тип возвращаемого значения: (A, B) -> C обозначает тип, представляющий функции, которые принимают два аргумента типов A и B и возвращают значение типа C. Список типов параметров может быть пустым, как в () -> A. Тип Unit возвращаемого значения нельзя опустить.

  • Типы функций могут иметь дополнительный необязательный тип получателя, который указывается перед точкой в записи: тип A.(B) -> C представляет функции, которые могут быть вызваны на объекте получателя A с параметром B и возвращают значение C. Лямбда-выражения с получателем часто используются вместе с этими типами.

  • Подвешенные функции относятся к специальному виду типов функций, которые имеют модификатор suspend в их записи, такой как suspend () -> Unit или suspend A.(B) -> C.

В записи типа функции можно необязательно включить имена параметров функции: (x: Int, y: Int) -> Point. Эти имена можно использовать для документирования значения параметров.

Чтобы указать, что тип функции может иметь значение null, используйте скобки следующим образом: ((Int, Int) -> Int)?.

Типы функций также могут быть объединены с помощью скобок: (Int) -> ((Int) -> Unit).

Запись со стрелкой является правоассоциативной, (Int) -> (Int) -> Unit эквивалентна предыдущему примеру, но не эквивалентна ((Int) -> (Int)) -> Unit.

Также можно присвоить типу функции альтернативное имя с помощью псевдонима типа:

typealias ClickHandler = (Button, ClickEvent) -> Unit

Создание экземпляров типа функции

Существует несколько способов получения экземпляра типа функции:

  • Используйте блок кода внутри лямбда-выражения в одном из следующих форматов:

    • a лямбда-выражение: { a, b -> a + b },

    • анонимная функция: fun(s: String): Int { return s.toIntOrNull() ?: 0 }

    Лямбда-выражения с получателем могут использоваться в качестве значений типов функций с получателем.

  • Используйте ссылку на вызываемый объект существующего объявления:

    • глобальную, локальную, членскую или расширяющую функцию: ::isOdd, String::toInt,

    • глобальную, членскую или расширяющую свойство: List<Int>::size,

    • конструктор: ::Regex

    Это включает в себя связанные вызываемые ссылки, которые указывают на член конкретного экземпляра: foo::toString.

  • Используйте экземпляры пользовательского класса, который реализует тип функции как интерфейс:

class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

Компилятор может вывести типы функций для переменных, если имеется достаточная информация:

val a = { i: Int -> i + 1 } // The inferred type is (Int) -> Int

Нелитеральные значения типов функций с и без получателя взаимозаменяемы, поэтому получатель может выступать в качестве первого параметра, и наоборот. Например, значение типа (A, B) -> C может передаваться или присваиваться там, где ожидается значение типа A.(B) -> C, и наоборот:

fun main() {
    //sampleStart
    val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
    val twoParameters: (String, Int) -> String = repeatFun // OK
    
    fun runTransformation(f: (String, Int) -> String): String {
        return f("hello", 3)
    }
    val result = runTransformation(repeatFun) // OK
    //sampleEnd
    println("result = $result")
}

Тип функции без получателя выводится по умолчанию, даже если переменная инициализируется ссылкой на расширяющую функцию. Чтобы изменить это, явно укажите тип переменной.

Вызов экземпляра типа функции

Значение типа функции может быть вызвано с помощью invoke(...) оператора: f.invoke(x) или просто f(x).

Если значение имеет тип получателя, объект получателя должен передаваться в качестве первого аргумента. Другой способ вызвать значение типа функции с получателем — добавить объект получателя перед ним, как если бы это была расширяющая функция: 1.foo(2).

Пример:

fun main() {
    //sampleStart
    val stringPlus: (String, String) -> String = String::plus
    val intPlus: Int.(Int) -> Int = Int::plus
    
    println(stringPlus.invoke("<-", "->"))
    println(stringPlus("Hello, ", "world!"))
    
    println(intPlus.invoke(1, 1))
    println(intPlus(1, 2))
    println(2.intPlus(3)) // extension-like call
    //sampleEnd
}

Встроенные функции

Иногда целесообразно использовать встроенные функции, которые обеспечивают гибкий контроль потока, для функций высшего порядка.

END_OF_DOCUMENT_MARKER

Лямбда-выражения и анонимные функции

Лямбда-выражения и анонимные функции являются литералами функций. Литералы функций — это функции, которые не объявляются, а передаются сразу как выражение. Рассмотрим следующий пример:

max(strings, { a, b -> a.length < b.length })

Функция max является функцией высшего порядка, так как она принимает значение функции в качестве своего второго аргумента. Этот второй аргумент — выражение, которое само по себе является функцией, называемой литералом функции, что эквивалентно следующей именованной функции:

fun compare(a: String, b: String): Boolean = a.length < b.length

Синтаксис лямбда-выражений

Полная синтаксическая форма лямбда-выражений следующая:

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
  • Лямбда-выражение всегда заключено в фигурные скобки.

  • Декларации параметров в полной синтаксической форме находятся внутри фигурных скобок и имеют необязательные аннотации типов.

  • Тело следует за ->.

  • Если выведенный тип возвращаемого значения лямбды не Unit, последнее (или возможно единственное) выражение внутри тела лямбды обрабатывается как возвращаемое значение.

Если вы опустите все необязательные аннотации, то останется следующее:

val sum = { x: Int, y: Int -> x + y }

Передача последующих лямбда-выражений

Согласно соглашению Kotlin, если последний параметр функции является функцией, то лямбда-выражение, передаваемое в качестве соответствующего аргумента, может быть размещено вне скобок:

val product = items.fold(1) { acc, e -> acc * e }

Такой синтаксис также известен как последующая лямбда.

Если лямбда является единственным аргументом в этом вызове, скобки можно полностью опустить:

run { println("...") }

it: неявное имя одного параметра

Очень часто лямбда-выражение имеет только один параметр.

Если компилятор может разобрать сигнатуру без каких-либо параметров, параметр не нужно объявлять, и -> можно опустить. Параметр будет неявно объявлен под именем it:

ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'

Возвращение значения из лямбда-выражения

Вы можете явно вернуть значение из лямбды, используя синтаксис квалифицированного возврата. В противном случае, значение последнего выражения неявно возвращается.

Следовательно, два следующих фрагмента эквивалентны:

ints.filter {
    val shouldFilter = it > 0
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0
    return@filter shouldFilter
}

Это соглашение, наряду с передачей лямбда-выражения вне скобок, позволяет использовать синтаксис в стиле LINQ:

strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }

Подчеркивание для неиспользуемых переменных

Если параметр лямбды не используется, вместо его имени можно поставить знак подчеркивания:

map.forEach { (_, value) -> println("$value!") }

Деструктуризация в лямбда-выражениях

Деструктуризация в лямбда-выражениях описывается как часть деструктурирующих деклараций.

Анонимные функции

Синтаксис лямбда-выражений выше опускает один момент — возможность указать тип возвращаемого значения функции. В большинстве случаев это не нужно, так как тип возвращаемого значения может быть выведен автоматически. Однако, если вам нужно его явно указать, вы можете использовать альтернативный синтаксис: анонимную функцию.

fun(x: Int, y: Int): Int = x + y

Анонимная функция очень похожа на обычное объявление функции, за исключением того, что у нее нет имени. Ее тело может быть либо выражением (как показано выше), либо блоком:

fun(x: Int, y: Int): Int {
    return x + y
}

Параметры и тип возвращаемого значения указываются так же, как и для обычных функций, за исключением того, что типы параметров можно опустить, если они могут быть выведены из контекста:

ints.filter(fun(item) = item > 0)

Вывод типа возвращаемого значения для анонимных функций работает так же, как и для обычных функций: тип возвращаемого значения выводится автоматически для анонимных функций с телом-выражением, но его нужно явно указать (или предполагается, что он Unit) для анонимных функций с телом-блоком.

При передаче анонимных функций в качестве параметров помещайте их в скобки. Сокращённый синтаксис, который позволяет опустить скобки, работает только для лямбда-выражений.

Другое различие между лямбда-выражениями и анонимными функциями заключается в поведении нелокальных возвратов. Оператор return без метки всегда возвращает из функции, объявленной с ключевым словом fun. Это означает, что return внутри лямбда-выражения вернёт из окружающей функции, а return внутри анонимной функции вернет из самой анонимной функции.

Замыкания

Лямбда-выражение или анонимная функция (а также локальная функция и объектное выражение) может получить доступ к своему замыканию, которое включает в себя переменные, объявленные во внешнем объёме. Переменные, захваченные в замыкании, могут быть изменены в лямбде:

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

Литералы функций с получателем

Типы функций с получателем, такие как A.(B) -> C, могут быть созданы со специальной формой литералов функций — литералами функций с получателем.

Как упоминалось выше, Kotlin предоставляет возможность вызывать экземпляр типа функции с получателем, предоставляя объект-получатель.

Внутри тела литерала функции, объект-получатель, переданный в вызов, становится неявным this, так что вы можете обращаться к членам этого объекта-получателя без каких-либо дополнительных квалификаторов или обратиться к объекту-получателю, используя this выражение.

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

Вот пример литерала функции с получателем вместе с его типом, где plus вызывается на объекте-получателе:

val sum: Int.(Int) -> Int = { other -> plus(other) }

Синтаксис анонимной функции позволяет вам указать тип получателя литерала функции напрямую. Это может быть полезно, если вам нужно объявить переменную типа функции с получателем и затем использовать её позже.

val sum = fun Int.(other: Int): Int = this + other

Лямбда-выражения могут использоваться в качестве литералов функций с получателем, когда тип получателя может быть выведен из контекста. Один из самых важных примеров их использования — безопасные по типу билдеры:

class HTML {
    fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // create the receiver object
    html.init()        // pass the receiver object to the lambda
    return html
}

html {       // lambda with receiver begins here
    body()   // calling a method on the receiver object
}
Последнее изменение: 21 февраля 2023 г.
Функции Встроенные функции

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/lambdas.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API