Функции высшего порядка и лямбда-выражения
Функции 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).
Вы также можете присвоить имени типу функции другое имя, используя псевдоним типа:
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-выражениями и анонимными функциями является поведение нелокальных возвратов. Оператор 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
}
© 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