Магия функций в Kotlin

В этой статье мы рассмотрим самые распространенные способы «магического» использования функций в Kotlin.

Extension Functions

Начнем с простого: расширения классов без наследования. Мы можем, не изменяя класс String и все использующие его пакеты, расширить этот класс, добавив новый метод или свойство (поле). Пусть у нас есть метод deleteSpaces():

private fun String.deleteSpaces(): String {
    return this.replace(" ", "")
}

И мы можем использовать этот метод так, как будто он является частью класса String. Пользователь увидит это так:

println("Hel lo, Wor ld".deleteSpaces()) // Hello,World

После компиляции же этот метод будет выглядеть примерно так (часть кода была упрощена, чтобы вам легче было понять суть):

private static final String deleteSpaces(@NotNull String receiver) {
    return receiver.replace(" ", "");
}

Отсюда можно сделать вывод, что внутри метода deleteSpaces() у нас есть доступ только к публичным полям и методам класса, благодаря чему инкапсуляция не нарушается.

Кроме Extension Functions в Kotlin по аналогии могут быть и Extension Properties:

private val String.first: Char
    get() {
        return this[0]
    }
print("Kotlin".first) // K

Большинство «магических» применений функций и лямбд в Kotlin, так же как и эта, являются не более чем синтаксическим сахаром, но каким удобным!

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

В Kotlin анонимные и лямбда-функции — это функции без имени, но при этом они могут быть использованы как объекты. Их можно писать прямо внутри выражения, минуя отдельное объявление.

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

{ аргументы -> возвращаемый_тип
    тело_функции
}

Синтаксис декларации анонимной функции полностью совпадает с синтаксисом обычной функции, но у первой нет имени.

fun defaultFun(x: Int, y: Int): Int = x + y // Именованная функция 
val f = fun(x: Int, y: Int): Int = x + y // Анонимная функция

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

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

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

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

private fun longWork(callback: (arg1: Int, arg2: String) -> Unit) {
    val result = doSomething()
    callback(result, "Kotlin > Java") // вызов callback
}

Она принимает в качестве аргумента функцию callback(), но вызывает ее только после функции doSomething(). Использование этой функции:

longWork({ arg1, arg2 -> Unit
    print("callback runned")
})

Здесь мы вызываем функцию longWork() и передаем ей в качестве аргумента лямбда-функцию, которую она вызовет позже. Koltin позволяет вынести лямбду за скобки, если она — последний аргументом функции, а также вовсе убрать скобки, если лямбда является единственным аргументом. Также в большинстве случаев можно убрать тип возвращаемого значения и заменить аргументы на _, если они не используются. Вот более короткий вариант:

longWork { _, _ -> 
    print("callback runned")
}

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

longWork {
    print("callback runned")
}

И это внешне напоминает уже не функцию высшего порядка, а языковую конструкцию, как например synchronized в Java. К слову, synchronized в Kotlin построен именно как функция высшего порядка.

Это очень удобно для создания так называемых DSL (Domain-Specific Languages) – предметно-ориентированных языков. Одни из самых популярных DSL для Kotlin — Anko (Android UI прямо в Kotlin с сохранением удобства XML-разметки), Gradle Kotlin DSL (Gradle-скрипты на Kotlin), kotlinx.html (по аналогии с Anko).

Для примера рассмотрим HTML-страницу на Kotlin:

System.out.appendHTML().html {
    body {
        div {
            a("http://kotlinlang.org") {
                target = ATarget.blank
                +"Main site"
            }
        }
    }
}

В stdout будет выведено:

<html>
  <body>
    <div><a href="http://kotlinlang.org" target="_blank">Main site</a></div>
  </body>
</html>

Главное преимущество этого DSL в том, что в отличие от декларативного HTML, в Kotlin есть переменные, которые могут быть использованы для генерации динамической страницы. И это намного красивее и удобнее классической генерации страницы через конкатенацию множества строк. В реальности для генерации HTML разметки используют другие методы, этот был показан только как пример DSL в Kotlin.

Другой пример использования функций высшего порядка – как аналог Streams API из Java:

listOf(1, 2, 3, 4, 5)
    .filter { n -> n % 2 == 1 } // 1, 3, 5
    .map { n -> n * n } // 1, 9, 25
    .drop(1) // 9, 25
    .take(1) // 9

Более сложные лямбды

Рассмотрим пример кода:

val builder = StringBuilder()
builder.append("Hello")
builder.append("World")
builder.append("Kotlin")
builder.append("The Best")
print(builder.toString())

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

StringBuilder().apply {
    append("Hello")
    append("World")
    append("Kotlin")
    append("The Best")
}.let {
    print(it.toString())
}

Как можно заметить, метод apply() позволяет не писать несколько раз builder.append() благодаря следующему прототипу метода:

public inline fun <T> T.apply(block: T.() -> Unit): T

Здесь лямбда-функция block — метод-расширение для типа T, в данном случае для StringBuilder. И append() внутри лямбды block — это this.append(), где this – экземпляр класса StringBuilder.

Метод let() действует схожим образом, только принимает немного другую лямбду:

public inline fun <T, R> T.let(block: (T) -> R): R

Здесь ссылка на объект передается не в качестве this, а в качестве явного аргумента метода, но мы его не указали. В таких случаях компилятор первый аргумент лямбда-функции автоматически называет «it».

Немного о недосказанном

Во-первых, в Kotlin, в отличие от Java, есть перегружаемые операторы. Так, например, если у класса есть метод plus(), то его можно вызвать оператором +, а метод get() – оператором [].

Во-вторых, функции в Kotlin могут быть явно помечены как inline или noinline. Этот модификатор сообщает компилятору, стоит ли заинлайнить функцию для увеличения производительности или нет. Но здесь кроется подвох: разное поведение return в inline и noinline функциях.

В inline функциях return будет произведен из ближайшей по области видимости noinline функции. В noinline – из самой функции. Эту проблему решают «именованные return».

Чтобы сделать return из лямбды, которую мы передаем в примере выше в apply(), можно использовать return@apply.

Именованными могут быть не только return, но и break, continue. Также можно создавать и собственные метки:

loop@ for (i in 1..100) {
    for (j in 1..100) {
        if (...) break@loop
    }
}

Кроме того, существует модификатор функции tailrec, который говорит компилятору заменить рекурсию в функции на цикл, если она написана в функциональном формате return if-then-else. Пример:

tailrec fun findFixPoint(x: Double = 1.0): Double = when {
    x == cos(x) -> x
    else -> findFixPoint(cos(x))
}

// Будет заменена при компиляции на
fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = cos(x)
        if (x == y) return x
        x = y
    }
}

В-третьих, в случае, если метод требует в качестве аргументов объект, который должен быть унаследован от класса/интерфейса с одним абстрактным методом, то в эту функцию можно передать лямбду или анонимную функцию, а компилятор сам создаст анонимный класс с переопределением абстрактного метода на нашу лямбду. Например, в стандартной библиотеке Android есть метод public void setOnClickListener(View.OnClickListener l), где OnClickListener – это интерфейс с единственным методом onClick(View v).

Лямбда, переданная в виде setOnClickListener { doSomething() }, будет скомпилирована в анонимный класс, реализующий интерфейс OnClickListener, где наша лямбда превратится в метод onClick(View v).

Итоги

Это далеко не всё о функциях в Kotlin, только самое часто используемое. Kotlin своими «магическими» функциями позволяет сильно упростить написание и, самое главное, чтение кода. Удобство написания и безопасность – это два самых важных отличия Kotlin от созданной ещё в 1995(!) году Java. В то время об удобстве и безопасности кода только мечтали.

Иван Борисов