Функции высшего порядка и лямбды в Kotlin
Функции в Kotlin являются функциями первого класса. Это значит, что они могут быть сохранены в переменные и структуры данных, переданы в качестве аргументов и возвращены из других функций высшего порядка. Вы можете использовать их любым способом, который возможен для других нефункциональных типов.
Функции высшего порядка
Функция высшего порядка - это функция, которая принимает функции как параметры или возвращает функцию в качестве результата. Хорошим примером такой функции является идиоматичная функция fold(), которая берёт первоначальное значение для объединения accumulator
и объединяющую функцию combine
и вычисляет возвращаемое значение путем последовательного применения объединяющей функции над текущим значением 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
возвращающей значение типа T
. Она вызывается внутри цикла for
, а результат ее выполнения записывается в accumulator
.
Если мы хотим вызвать функцию fold()
, нам нужно передать другую функцию в качестве аргумента, и лямбда-функции (описанные ниже) хорошо подходят для этой цели:
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)
Дальше в статье концепции, упомянутые выше, будут рассмотрены более подробно.
Функциональные типы
Kotlin использует функциональные типы, такие как (Int) -> String
, для объявлений, в которые используются функции высшего порядка:
val onClick: () -> Unit = ...
Эти типы имеют особый вид, который описывает сигнатуру функции, т. е. ее параметры и возвращаемое значение:
– Все функции имеют список типов параметров, заключенный в круглые скобки, и тип возвращаемого значения: (A, B) -> C
– это функция, которая берет два аргумента с типами A
и B
и возвращает значение типа C
. Список типов параметров может быть пустым: () -> A
, но тип возвращаемого значения Unit
не может быть опущен.
– Функциональные типы могут также иметь дополнительный тип объекта-приемника, который указывается перед точкой и названием функции: A.(B) -> C
представляет собой функцию, которая может быть вызвана у объекта-приемника типа A
с аргументом типа B
и возвращает значение типа C
.
– Приостанавливаемые (suspend) функции принадлежат к особому виду функций, которые имеют модификатор suspend в сигнатуре: suspend () -> Unit
или suspend A.(B) -> C
.
Тип функции может опционально включать в себя имена параметров функции: (x: Int, y: Int) -> Point
. Эти имена могут быть использованы для указания назначения параметров.
Чтобы указать, что значение функции может быть null, используйте круглые скобки: (x: Int, y: Int) -> Point
.
Функциональные типы могут объединяться: (Int) -> ((Int) -> Unit)
.
Стрелка в функциональном типе право-ассоциативна. Тип (Int) -> (Int) -> Unit
эквивалентен типу из предыдущего примера, но не типу ((Int) -> (Int)) -> Unit
.
Вы также можете дать функциональному типу альтернативное имя, используя typealias:
typealias ClickHandler = (Button, ClickEvent) -> Unit
Создание экземпляра функции
Существует несколько способов получения экземпляра функционального типа:
– Использование блока кода внутри функционального литерала в одной из этих форм:
- лямбда-выражение:
{ a, b -> a + b }
, - анонимная функция:
fun(s: String): Int { return s.toIntOrNull() ?: 0 }
Функциональные литералы с объектом-приемником (см. ниже) могут быть использованы в качестве функциональных типов с объектом-приемником.
– Использование ссылки на существующую функцию:
- функцию высшего порядка, локальную функцию, функцию класса или extension-функцию:
::isOdd
,String::toInt
, - свойство высшего порядка, свойство класса или extension-свойство:
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
Вызов объекта функционального типа
Значение функционального типа может быть вызвано (т. е. данная функция может быть запущена) путем использования оператора invoke: f.invoke(x)
или просто f(x)
.
Если функциональный тип имеет объект-приемник, то этот объект должен быть передан в качестве первого аргумента. Другой способ вызова значения такого типа – использование его как extension-функции: 1.foo(2)
.
Пример:
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
Инлайн-функции
Иногда выгодно улучшить производительность функций высшего порядка, используя инлайн функции.
Лямбда-выражения и анонимные функции
Лямбда-выражение и анонимная функция – это "функциональные литералы", то есть необъявленные функции, которые немедленно используются в качестве выражения. Рассмотрим следующий пример:
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, y -> x + y }
Передача лямбды последним аргументом
В Kotlin существует конвенция: если последний параметр функции является функцией, то лямбда-выражение, переданное последним аргументом, может быть вынесено за круглые скобки:
val product = items.fold(1) { acc, e -> acc * e }
Если лямбда – единственный аргумент функции, то круглые скобки могут быть полностью опущены:
run { println("...") }
Ключевое слово it
Ещё одна полезная конвенция состоит в том, что если функциональный литерал имеет ровно один параметр, его объявление можно удалить (вместе с ->
), и обращаться к нему по имени it
:
ints.map { it * 2 }
Эти конвенции позволяют писать код в стиле LINQ:
strings.filter { it.length == 5 } .sortBy { it } .map { it.toUpperCase() }
Возврат значения из лямбды
Мы можем явно вернуть значение из лямбды, используя qualified return синтаксис. Иначе неявно возвращается значение последнего выражения в лямбде. Поэтому два следующих примера эквиваленты:
ints.filter { val shouldFilter = it > 0 shouldFilter } ints.filter { val shouldFilter = it > 0 return@filter shouldFilter }
Обратите внимание, что функция принимает другую функцию в качестве своего последнего и единственного параметра, поэтому круглые скобки опущены.
Символ подчеркивания
Если параметр лямбды не используется, разрешено применять подчеркивание вместо его имени
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
, в свою очередь, приведет к выходу из анонимной функции.
Замыкания
Лямбда-выражение или анонимная функция (так же, как и локальная функция или object expression) имеет доступ к своему замыканию, то есть к переменным, объявленным вне этого выражения или функции. В отличие от Java, переменные, захваченные в замыкании, могут быть изменены:
var sum = 0 ints.filter { it > 0 }.forEach { sum += it } print(sum)
Литералы функций с объектом-приёмником
Kotlin предоставляет возможность вызывать литерал функции с указанным объектом-приёмником. Внутри тела литерала вы можете вызывать методы объекта-приёмника без дополнительных определителей. Это схоже с принципом работы расширений, которые позволяют получить доступ к членам объекта-приёмника внутри тела функции. Один из самых важных примеров использования литералов с объектом-приёмником это Type-safe Groovy-style builders.
Тип такого литерала — это тип функции с приёмником:
sum : Int.(other: Int) -> Int
По аналогии с расширениями, литерал функции может быть вызван так, будто он является методом объекта-приёмника:
1.sum(2)
Синтаксис анонимной функции позволяет вам явно указать тип приёмника. Это может быть полезно в случае, если вам нужно объявить переменную типа нашей функции для использования в дальнейшем.
val sum = fun Int.(other: Int): Int = this + other
Лямбда-выражения могут быть использованы как литералы функций с приёмником, когда тип приёмника может быть выведен из контекста. Одним из самых важных примеров их использования являются типобезопасные билдеры:
class HTML { fun body() { ... } } fun html(init: HTML.() -> Unit): HTML { val html = HTML() // создание объекта-приёмника html.init() // передача приёмника в лямбду return html } html { // лямбда с приёмником начинается тут body() // вызов метода объекта-приёмника }
Источники:
Высокоуровневые функции и лямбды
Higher-Order Functions and Lambdas