Высокоуровневые функции и лямбды
В Kotlin функции являются функциями первого класса. Это значит, что они могут храниться в переменных и структурах данных, передаваться в качестве аргументов и возвращаться из других функций высшего порядка. Вы можете работать с функциями любым способом, который возможен для других нефункциональных значений.
Чтобы это облегчить, Kotlin, как статически типизированный язык программирования, использует семейство функциональных типов для представления функций и предоставляет набор специализированных языковых конструкций, таких как лямбда-выражения.
Функции высшего порядка
Функция высшего порядка - это функция, которая принимает функции как параметры, или возвращает функцию в качестве результата.
Хорошим примером такой функции является идиома функционального программирования fold
для коллекций, которая
принимает начальное значение - accumulator
вместе с комбинирующей функцией и строит возвращаемое значение,
последовательно комбинируя текущее значение accumulator
с каждым элементом коллекции, заменяя значение accumulator
.
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() {
val items = listOf(1, 2, 3, 4, 5)
// Лямбда - это блок кода, заключенный в фигурные скобки.
items.fold(0, {
// Если у лямбды есть параметры, то они указываются перед знаком '->'
acc: Int, i: Int ->
print("acc = $acc, i = $i, ")
val result = acc + i
println("result = $result")
// Последнее выражение в лямбде считается возвращаемым значением:
result
})
// Типы параметров в лямбде необязательны, если они могут быть выведены:
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
// Ссылки на функции также могут использоваться для вызовов функций высшего порядка:
val product = items.fold(1, Int::times)
println("joinedToString = $joinedToString")
println("product = $product")
}
Функциональные типы
Kotlin использует семейство функциональных типов, таких как (Int) -> String
, для объявлений, которые являются частью
функций: val onClick: () -> Unit = ...
.
Эти типы имеют специальные обозначения, которые соответствуют сигнатурам функций, то есть их параметрам и возвращаемым значениям:
- У всех функциональных типов есть список с типами параметров, заключенный в скобки, и возвращаемый тип:
(A, B) -> C
обозначает тип, который предоставляет функции два принятых аргумента типаA
иB
, а также возвращает значение типаC
. Список с типами параметров может быть пустым, как, например, в() -> A
. Возвращаемый типUnit
не может быть опущен;
- У функциональных типов может быть дополнительный тип - получатель (ориг.: receiver), который указывается в
объявлении перед точкой: тип
A.(B) -> C
описывает функции, которые могут быть вызваны для объекта-получателяA
с параметромB
и возвращаемым значениемC
. Литералы функций с объектом-приёмником часто используются вместе с этими типами;
- Останавливаемые функции (ориг.: suspending functions)
принадлежат к особому виду функциональных типов, у которых в объявлении присутствует модификатор
suspend
, например,suspend () -> Unit
илиsuspend A.(B) -> C
.
Объявление функционального типа также может включать именованные параметры: (x: Int, y: Int) -> Point
.
Именованные параметры могут быть использованы для описания смысла каждого из параметров.
Чтобы указать, что функциональный тип может быть nullable,
используйте круглые скобки: ((Int, Int) -> Int)?
.
При помощи круглых скобок функциональные типы можно объединять: (Int) -> ((Int) -> Unit)
.
Стрелка в объявлении является правоассоциативной (ориг.: right-associative), т.е. объявление
(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 } // Выведенный тип - (Int) -> Int
Небуквальные (ориг.: non-literal) значения функциональных типов с и без получателя являются взаимозаменяемыми,
поэтому получатель может заменить первый параметр, и наоборот. Например, значение типа (A, B) -> C
может быть передано
или назначено там, где ожидается A.(B) -> C
, и наоборот.
fun main() {
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
println("result = $result")
}
Обратите внимание, что функциональный тип без получателя выводится по умолчанию, даже если переменная инициализируется со ссылкой на функцию-расширение. Чтобы это изменить, укажите тип переменной явно.
Вызов экземпляра функционального типа
Значение функционального типа может быть вызвано с помощью оператора invoke(...)
:
f.invoke(x)
или просто f(x)
.
Если значение имеет тип получателя, то объект-приёмник должен быть передан в качестве первого аргумента. Другой способ
вызвать значение функционального типа с получателем - это добавить его к объекту-приёмнику, как если бы это была
функция-расширение: 1.foo(2)
.
Пример:
fun main() {
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)) // вызывается как функция-расширение
}
Встроенные функции
Иногда выгодно улучшить производительность функций высшего порядка, используя встроенные функции (ориг.: inline functions).
Лямбда-выражения и анонимные функции
Лямбда-выражения и анонимные функции - это “функциональный литерал”, то есть необъявленная функция, которая немедленно используется в качестве выражения. Рассмотрим следующий пример:
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 } // этот литерал имеет тип '(it: Int) -> Boolean'
Возвращение значения из лямбда-выражения
Вы можете вернуть значение из лямбды явно, используя оператор return. Либо неявно будет возвращено значение последнего выражения.
Таким образом, два следующих фрагмента равнозначны:
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
(non-local returns).
Слово 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
Лямбда-выражения могут быть использованы как литералы функций с приёмником, когда тип приёмника может быть выведен из контекста. Один из самых важных примеров их использования это типобезопасные строители (ориг.: type-safe builders).
class HTML {
fun body() { ... }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // создание объекта-приёмника
html.init() // передача приёмника в лямбду
return html
}
html { // лямбда с приёмником начинается тут
body() // вызов метода объекта-приёмника
}