December 19, 2019

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