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