Функциональные типы и лямбды в Kotlin #

Васильев Андрей Михайлович, 2024

Версии презентации

Функции #

  • В Kotlin функции являются объектами первого класса, т.е. их можно сохранять в переменные, получать в качестве аргументов и возвращать в качестве результатов фукнции
  • Т.е. с функциями можно работать любым способом, доступным для других значений
  • Функцию, помещённую в переменную можно вызвать как и обычную функцию, ()
  • Для представления того, что значение является функцией необходимо описать её как функциональный тип: (Int) -> Int
fun doSome(value: Int, modifier: (Int) -> Int): Int {
    val result: Int = modifier(value)
    return result
}

Функциональный тип вызывается с помощью оператора invoke

  • Либо явно: modifier.invoke(55)
  • Либо неявно: modifier(33)

Функциональные типы #

  • У всех функциональных типов есть список с типами параметров, заключённых в скобки, и возвращаемый тип, записываемый после стрелки (A, B) -> C
    • (Int, Double) -> String — функциональный тип, принимающий два числа и возвращающий строку
    • () -> Unit — функциональный тип, не принимающий никаких аргументов и ничего не возвращающий. Unit не может быть опущен
  • Объявление функционального типа также может включать именованные параметры: (x: Int, y: Int) -> Point
  • Чтобы указать, что функциональный тип может принимать null-значения, необходимо использовать круглые скобки: ((Int, Int) -> Int)?
  • Вы также можете присвоить функциональному типу альтернативное имя, используя псевдонимы типов
    typealias ClickHandler = (Button, ClickEvent) -> Unit
    fun addClickHandler(handler: ClickHandler) { ... }

Создание функционального типа #

Если мы хотим создать новый блок кода, который можно сохранить (или передать):

  • Используя лямбда-выражение: { a, b -> a + b}
  • Используя анонимную функцию: fun(a: Int, b: Int): Int = a + b

В лямбда-выражении можно опустить типы, если компилятор сможет корректно их вывести относительно места их использования

val result = doSome(
    34,
    { num -> num + 42 },
)
println(result)

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

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

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

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
  • Лямбда-выражение всегда заключено в скобки {...}
  • Объявление параметров при таком синтаксисе происходит внутри этих скобок и может включать в себя аннотации типов
  • Тело функции начинается после знака ->
  • Тело может состоять из нескольких выражений как и обычная функция
  • Если тип возвращаемого значения не Unit, то в качестве возвращаемого типа принимается последнее (а возможно и единственное) выражение внутри тела лямбды

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

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

Возвращение значения из лямбда-выражения #

  • Вы можете вернуть значение из лямбды явно, используя оператор return
  • Либо неявно будет возвращено значение последнего выражения

Таким образом, два следующих фрагмента равнозначны:

ints.filter {
    val shouldFilter = it > 0
    shouldFilter
}
ints.filter {
    val shouldFilter = it > 0
    return@filter shouldFilter
}
  • Вторую форму удобно использовать в случае, если возможно несколько логических выходов из одного блока
  • Блоку кода также может быть присвоена метка, что удобно при работе с вложенными лямбда-выражениями
ints.filter positive@{
    val shouldFilter = it > 0
    return@positive shouldFilter
}

return без метки совершит выход из функции, не из лямбда-выражения

Передача лямбды в качестве последнего параметра #

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

val product = items.fold(1) { acc, e -> acc * e }

Такой синтаксис также известен как trailing lambda

Когда лямбда-выражение является единственным аргументом функции, круглые скобки могут быть опущены

run { println("...") }

Стандартная библиотека по работе с коллекциями предлагает много функций, которые предполагают работу с лямбда-выражениями

it — неявное имя единственного параметра #

  • Очень часто лямбда-выражение имеет только один параметр
  • Если компилятор способен самостоятельно определить сигнатуру, то объявление параметра можно опустить вместе с ->
  • Параметр будет неявно объявлен под именем it
ints.filter { it > 0 } // этот литерал имеет тип '(it: Int) -> Boolean'

Символ подчеркивания для неиспользуемых переменных #

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

map.forEach { _, value -> println("$value!") }

Замыкания #

  • Лямбда-выражение или анонимная функция (так же, как и локальная функция или анонимные объекты) имеет доступ к своему замыканию, то есть к переменным, объявленным вне этого выражения или функции
  • Переменные, захваченные в замыкании, могут быть изменены в лямбде
var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

Обычно можно встретить в функциях-генераторах:

typealias Modifier = (Int) -> Int
fun createNumberAdder(howMuch: Int): Modifier = {
    number + howMuch
}

Реализация функционального типа через классы #

  • Функции-генераторы могут быть эффективно заменены классами, которые создают объекты, согласующиеся с функциональным типом
  • В классе необходимо реализовать оператор invoke
typealias Modifier = (Int) -> Int

class NumberAdder(
    private val howMuch: Int,
): Modifier {
    override fun invoke(number: Int): Int {
        return number + howMuch
    }
}

val twoAdder = NumberAdder(2)
println(twoAdder(42)) // "44"

Функциональные (SAM) интерфейсы #

  • Интерфейсы только с одним абстрактным методом называются функциональными интерфейсами или Single Abstract Method (SAM) интерфейсами
  • Функциональный интерфейс может иметь несколько неабстрактных членов, но только один абстрактный

Чтобы объявить функциональный интерфейс, используйте модификатор fun.

fun interface KRunnable {
   fun invoke()
}

Классы, которые его реализуют, должны будут реализовать только одну функцию

Использование SAM-интерфейса #

Рассмотрим следующий SAM-интерфейс по манипулированию целыми числами

fun interface IntManipulator {
    fun manipulate(input: Int): Int
}

Реализуем этот интерфейс с помощью лямбда-выражения

val twoMultiplier = IntManipulator { it * 2 }

Реализуем этот интерфейс с помощью классов

class NumberMultiplier(private val multiplicator)
        : IntManipulator {
    override fun manipulate(input: Int) {
        return input * multiplicator
    }
}
val twoMultiplier = NumberMultiplier(2)

Функциональные интерфейсы и псевдонимы типов #

Функциональные интерфейсы и псевдонимы типов служат разным целям

  • Псевдонимы типов – это просто имена существующих типов, они не создают новый тип, в то время как функциональные интерфейсы делают это
  • Вы можете предоставить расширения, специфичные для конкретного функционального интерфейса, которые будут неприменимы для простых функций или их псевдонимов типов
  • Псевдонимы типов могут иметь только один элемент, в то время как функциональные интерфейсы могут иметь несколько неабстрактных элементов и один абстрактный элемент
  • Функциональные интерфейсы также могут реализовывать и расширять другие интерфейсы
  • Функциональные интерфейсы более гибкие и предоставляют больше возможностей, чем псевдонимы типов, но они могут быть более дорогостоящими как синтаксически, так и во время выполнения, поскольку могут потребовать преобразования в определенный интерфейс