Spec-Zone.ru › Kotlin 1.4

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

Функции 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
}

Встраиваемые функции

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

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

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

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 }

Такой синтаксис также известен как trailing lambda.

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

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.toUpperCase() }

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

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

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

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

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

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

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

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
}

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

Spec-Zone.ru

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