Высокоуровневые функции и лямбды

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

Высокоуровневая функция - это функция, которая принимает другую функцию в качестве входного аргумента, либо имеет функцию в качестве возвращаемого результата. Хорошим примером такой функции является lock(), которая берёт залоченный объект и функцию, применяет лок, выполняет функцию и отпускает lock:

fun <T> lock(lock: Lock, body: () -> T): T{
  lock.lock()
  try{
      return body()
  }
  finally {
      lock.unlock()
  }
}

Давайте проанализируем этот блок. Параметр body имеет функциональный тип: () -> T, то есть предполагается, что это функция, которая не имеет никаких входных аргументов и возвращает значение типа T. Она вызывается внутри блока try, защищена lock, и её результат возвращается функцией lock().

Если мы хотим вызвать метод lock(), мы можем подать другую функцию в качестве входящего аргумента (более подробно читайте Ссылки на функции):

fun toBeSynchronized() = sharedResource.operation()

val result = lock (lock, ::toBeSynchronized)

Другой, наиболее удобный способ применения лямбда-выражения:

val result = lock(lock, { sharedResource.operation() })

Лямбда-выражения более подробно описаны здесь, но в целях продолжить этот раздел, давайте произведём краткий обзор:

  • Лямбда-выражения всегда заключены в фигурные скобки,
  • Параметры этого выражения (если такие есть) объявлены до знака -> (параметры могут быть опущены),
  • Тело выражения идёт после знака ->.

В Kotlin существует конвенция, по которой, если последний параметр функции является функцией, и вы применяете лямбда- выражение в качестве аргумента, вы можете указать её вне скобок:

lock (lock) {
    sharedResource.operation()
}

Другим примером функции высшего порядка служит функция map():

fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
    val result = arrayListOf<R>()
    for (item in this)
        result.add(transform(item))
    return result
}

Эта функция может быть вызвана следующим образом:

val doubled = ints.map { it -> it * 2 }

Обратите внимание, что параметры могут быть проигнорированы при вызове функции в том случае, если лямбда является единственным аргументом для её вызова.

Ключевое слово it: неявное имя единственного параметра

Ещё одной полезной особенностью синтаксиса является возможность опустить объявление параметра функции в случае, если он единственный (вместе с ->). Слово it будет принято в качестве имени для такой функции:

ints.map { it * 2 }

Это соглашение позволяет писать код в LINQ стиле:

strings.filter { it.lenght == 5 }.sortBy { it }.map { it.toUpperCase() }

Инлайн функции

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

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

Лямбда-выражения или анонимные функции являются "функциональными константами"(ориг. "functional literal"), то есть функциями, которые не были объявлены, но сразу были переданы в качестве выражения. Рассмотрим следующий пример:

max(strings, { a, b -> a.length < b.length })

Функция max - высокоуровневая функция, так как она принимает другую функцию в качестве входного аргумента. Этот второй аргумент является выражением, которое само по себе представляет из себя функцию, то есть functional literal.

fun compare(a: String, b: String): Boolean = a.length < b.length

Типы функций

Для того, чтобы функция принимала другую функцию в качестве входного параметра, нам необходимо указать её (входящей функции) тип. К примеру, вышеуказанная функция max определена следующим образом:

fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
    var max: T? = null
    for (it in collection)
        if (max == null || less(max, it))
            max = it
    return max
}

Параметр 'less' является (T, T) -> Boolean типом, то есть функцией, которая принимает два параметра типа T и возвращает 'Boolean':'true', если первый параметр меньше, чем второй.

В теле функции, строка 4, less используется в качестве функции: она вызывается путём передачи двух аргументов типа T.

Тип функции может быть написан так, как указано выше, или же может иметь определённые параметры, если вы хотите обозначить значения каждого из параметров.

val compare: (x: T, y: T) -> Int = ...

Синтаксис лямбда-выражений

Полная синтаксическая форма лямбда-выражений, таких как literals of function types, может быть представлена следующим образом:

val sum = { x: Int, y: Int -> x + y }

Лямбда-выражение всегда заключено в скобки {...}, объявление параметров при таком синтаксисе происходит внутри этих скобок и может включать в себя аннотации типов (опционально), тело функции начинается после знака ->. Если тип возвращаемого значения не Unit, то в качестве возвращаемого типа принимается последнее (а возможно и единственное) выражение внутри тела лямбды.

Если мы вынесем все необязательные объявления, то, что останется, будет выглядеть следующим образом:

val sum: (Int, Int) -> Int = { x, y -> x + y }

Обычное дело, когда лямбда-выражение имеет только один параметр. Если Kotlin может определить сигнатуру метода сам, он позволит нам не объявлять этот единственный параметр, и объявит его сам под именем it:

ints.filter { it > 0 } //Эта константа имеет тип '(it: Int) -> Boolean'

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

ints.filter {
    val shouldFilter = it > 0 
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0 
    return@filter shouldFilter
}

Обратите внимение, что функция принимает другую функцию в качестве своего последнего параметра, аргумент лямбда-выражения в таком случае может быть принят вне списка аргументов, заключённого в скобках. См. callSuffix.

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

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

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()   // вызов метода объекта-приёмника
}