Как стать автором
Обновить

Kotlin и свои почти языковые конструкции

Время на прочтение 5 мин
Количество просмотров 29K
Скорее всего, из разработчиков, пользующихся Java, и в особенности Android-разработчиков многие уже знают про Kotlin. Если нет, то никогда не поздно узнать. Особенно если Java не устраивает вас чем-то как язык — что наверняка так — или если вы владеете Scala, но и этот язык вам не подходит, что тоже не исключено.

Если кратко, то Kotlin — это статически типизированный язык, ориентирующийся на JVM, Android (компилируется в байт-код Java) и веб (компилируется в JavaScript). JetBrains, разработчик языка, ставили своей целью лаконичный и понятный синтаксис, быструю компиляцию кода и типобезопасность. Язык пока находится в предрелизном состоянии, но всё стремительно движется к релизу.

К слову, после Java «переучиться» на Kotlin не составит никакого труда, в этом поможет и понятный (субъективно) синтаксис, и полная совместимость с кодом на Java в обе стороны, что позволяет Java-программисту использовать весь привычный набор библиотек.

Ещё одной целью разработчиков языка была возможность его гибкого использования, в том числе для создания библиотек, внешне похожих на DSL, и собственных конструкций (хороший пример типобезопасного builder'а для HTML; статья про реализацию yield). У Kotlin есть несколько фич, которые позволят решать эти задачи эффективно и красиво. Давайте с ними познакомимся.

Расширения (Extensions)


В Kotlin есть возможность дополнять функционал произвольного класса, не наследуясь от него, функциями (и свойствами) расширения. Такая же возможность есть, например, в C#. Стоит отметить, что поведение функций расширения отличается от member functions: вызовы функций расширения разрешаются статически, по объявленному типу, а не виртуально.

Пример:

fun String.words(): List<String> {
    return this.split("\\W".toRegex())
}

//сокращённая запись функции, состоящей только из return statement
fun <T> List<T>.rotate(n: Int): List<T> = drop(n) + take(n)

val str = "a quick brown fox jumps over the lazy dog"
val words = s.words() 

val yoda = words.rotate(5)
println(yoda.joinToString(" ") // over the lazy dog a quick brown fox jumps

toRegex(), drop(n), take(n) и joinToString(" ") в примере — это тоже функции расширения.

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


1. Функцию экземпляра или функцию расширения, имеющую только один аргумент, можно вызывать в инфиксной форме:

val squares = (1..100) map { i -> i * i } 
//эквивалентно (1..100).map({i -> i * i })

val multOfThree = squares filter { it % 3 == 0 } 
//it можно использовать в лямбда-выражении с одним аргументом для его обозначения

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

val list = arrayListOf(1, 2, 3) 
with(list) {
    add(4)
    add(5)
    add(6)
    removeIf { it % 2 == 0 }
}
//эквивалентно with(list, { ... })

Встраиваемые (inline) функции


В Kotlin есть аннотация @inline, которой можно пометить функцию. После этого при компиляции код этой функции, и её функциональных аргументов будет подставляться в места вызова. С одной стороны, это даёт некоторые новые возможности (non-local return, reified generics), с другой — есть ограничение, что функциональные аргументы inline-функции в её теле можно только вызывать или передавать в другие inline-функции. Основной же действие @inline — на производительность: происходит меньше вызовов функций и, что важно, не создаются анонимные классы и их объекты для каждого лямбда-выражения.

Большая часть функций расширения из стандартной библиотеки вроде тех же map и filter.

Небольшой пример:

@inline fun <T> Iterable<T>.withEach(action: T.() -> Unit) = forEach { it.action() }

//в теле метода:
var i = 0
val lists = (0..5) map { ArrayList<Int>() }
lists.withEach { add(++i) }

Несмотря на то, что этот код пестрит лямбда-выражениями, ни одного анонимного класса для них создано не будет, и i даже не попадёт в closure. Просто праздник!

Попробуем?


Посмотрим, что мы можем сделать со всем этим арсеналом — предположим, что мы хотим сделать такую довольно бесполезную конструкцию:

val a = someInt()
val b = someList()

val c = (a % b.size()) butIf (it < 0) { it + b.size() }
//аналогично (a % b.size()) let { if (it < 0) it + b.size() else it }

Прямо так, к сожалению, не получится, но постараемся сделать что-то похожее.

Первая попытка: функция с двумя функциональными аргументами


fun <T> T.butIf(condition: (T) -> Boolean, thenFunction: (T) -> T): T {
    if (condition(this)) {
        return thenFunction(this)
    }
    return this
}

Вот так её можно использовать:

val c = (a % b.size()).butIf({it < 0}) {it + b.size()}

Если сюда добавить inline, должно получиться достаточно эффективно. Позже посмотрим, насколько, а пока попробуем добиться более красивого синтаксиса для этой конструкции.

Вторая попытка: красивый синтаксис


abstract class _ButIfPrefix<T>
constructor(var originalValue: T) {
    abstract fun then(thenFunction: (T) -> T): T

    object trueBranch : _ButIfPrefix<Any?>(null) {
        override final inline fun then(thenFunction: (Any?) -> Any?) = thenFunction(originalValue)
    }

    object falseBranch : _ButIfPrefix<Any?>(null) {
        override final inline fun then(thenFunction: (Any?) -> Any?) = originalValue
    }
}

fun <T> T.butIf(condition: (T) -> Boolean): _ButIfPrefix<T> {
    val result = (if (condition(this))
        _ButIfPrefix.trueBranch else
        _ButIfPrefix.falseBranch) as _ButIfPrefix<T>
    result.originalValue = this
    return result
}

Этот вариант не рассчитан на многопоточность! Для использования его в нескольких потоках нужно будет завернуть экземпляры в ThreadLocal, что ещё немного ухудшит производительность.

Здесь будет цепочка из двух инфиксных вызовов, первый — функция расширения на самом объекте, второй — функция экземпляра _ButIfPrefix. Пример использования:

val c = (a % b.size()) butIf { it < 0 } then { it + b.size() }

Третья попытка: каррирование


Попробуем так:

fun <T> T.butIf0(condition: (T) -> Boolean): ((T) -> T) -> T {
    return inner@ { thenFunction ->
        return@inner if (condition(this)) thenFunction(this) else this
    }
}

Использование:

val c = (a % b.size()).butIf { it < 0 } ({ it + b.size() })

По сравнению с первой попыткой изменилось расположение скобок в вызове. :)
Учитывая inline, мы можем ожидать, что работать этот вариант будет так же, как первый.
Это можно проверить, взглянув на байт-код: у IntelliJ IDEA есть утилита, показывающая байт-код, в который скомпилируется код на Kotlin, на лету, и даже можно посмотреть, как будет отличаться байт-код с @inline и без.

Производительность


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

Тестировать будем на таком примере:

val range = -20000000..20000000
val list = ArrayList<Int>()
//warm-up
for (i in range) { 
    list add i % 2
}
list.clear()

val timeBefore = System.currentTimeMillis()
for (i in range) {
    val z = (i % 2) butIf { it < 0 } then { it + 2 } //и аналоги
    list add z
}
println("${System.currentTimeMillis() - timeBefore} ms")

Задно добавим к сравнению такой код, который будет эталоном производительности:

...
val d = it % 2
val z = if (d < 0) d + 2 else d
...

По итогам пятидесятикратного запуска с последующим усреднением времени получилась следующая таблица:

Реализация Без inline C inline
Эталон 319 ms
I попытка 406 ms 348 ms
II попытка 610 ms 520 ms
II попытка с ThreadLocal 920 ms 876 ms
III попытка 413 ms 399 ms

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

Итого


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

Удачи!
Теги:
Хабы:
+20
Комментарии 31
Комментарии Комментарии 31

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн