Функциональные типы и лямбды в 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)Функциональные интерфейсы и псевдонимы типов #
Функциональные интерфейсы и псевдонимы типов служат разным целям
- Псевдонимы типов – это просто имена существующих типов, они не создают новый тип, в то время как функциональные интерфейсы делают это
 - Вы можете предоставить расширения, специфичные для конкретного функционального интерфейса, которые будут неприменимы для простых функций или их псевдонимов типов
 - Псевдонимы типов могут иметь только один элемент, в то время как функциональные интерфейсы могут иметь несколько неабстрактных элементов и один абстрактный элемент
 - Функциональные интерфейсы также могут реализовывать и расширять другие интерфейсы
 - Функциональные интерфейсы более гибкие и предоставляют больше возможностей, чем псевдонимы типов, но они могут быть более дорогостоящими как синтаксически, так и во время выполнения, поскольку могут потребовать преобразования в определенный интерфейс