Spec-Zone.ru › Kotlin 1.7

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

Функции 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. Эти имена могут быть использованы для документирования значения параметров.

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

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

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

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

typealias ClickHandler = (Button, ClickEvent) -> Unit

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

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

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

    • лямбда-выражение: { 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
}

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

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

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

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

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

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

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

Синтаксис lambda-выражений

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

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

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

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

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

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

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

Передача trailing-лямбда

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

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

Такой синтаксис также известен как trailing-лямбда.

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

run { println("...") }

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

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

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

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

Возвращение значения из lambda-выражения

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Замыкания

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

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

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

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
}
Последнее изменение: 06 сентября 2022
Функции Встроенные функции

© 2010–2022 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