Если кратко, то 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
может помочь улучшить ситуацию, если в вашем коде есть функции первого порядка. В любом случае, думаю, у вас найдутся хорошие сценарии применения для всего этого.Удачи!
Комментарии (31)
vintage
14.09.2015 17:01Посмотрите на реализацию mixin и mixin template в D.
burjui
14.09.2015 19:04+1Пожалуй, на первое смотреть не стоит, потому что метапрограммирование через конкатенацию строк ничем принципиально не лучше сишных макросов. Надеюсь, в D когда-нибудь впилят нормальные AST macros вместо этого костыля, всё-таки 2015 год на дворе, а тот же Scheme это уже 40 лет как умеет (понятно, что в Lisp'ах это реализуется проще, но всё же). Я иногда использую template mixin, но строковый mixin я старательно избегаю, т.к. код генерации строк для mixin обычно выглядит, как синтаксическая блевотина, а попробуешь использовать std.string.format() для генерации строк, так получишь новую проблему — не забыть экранировать %, если генерируемый код использует форматирование или деление по модулю (x % 2). И даже не надейтесь на помощь IDE, т.к. для корректного парсинга сгенерированного кода в IDE нужно засунуть компилятор D. В общем, mixin лучше использовать только в крайнем случае, т.к. отлаживать сгенерённый фарш просто невозможно, а ошибки его компиляции могут посоревноваться в бесполезности с оными для шаблонов C++.
vintage
14.09.2015 23:03Разумеется по возможности стоит использовать mixin-template. А mixin — в тандеме с token-strings. Конкретно mixin хорош тем, что можно на лету транслировать например HTML-шаблон в код на D и закешировать на диске, а потом просто подключать этот код из файла. При этом всегда есть возможность глянуть что там нагенерилось в нормальном виде (на D), а не в AST. А то, что LISP код — это практически AST — это, конечно, круто, только это явно не Kotlin :-)
Stiver
15.09.2015 07:57Со своей колокольни: никогда не чувствовал потребности в подобных красивостях — там скобочки сэкономить, здесь стрелочку дописать… один черт. Что по-настоящему нужно в Java — это возможность возвращать из функции несколько значений, например как Matlab это разрешает. Вот что было бы здорово.
h0tkey
15.09.2015 09:57Про скобочки, пожалуй, правда ваша, в тех разумных пределах, пока синтаксический шум не мешает читать код.
А насчёт возврата нескольких значений — Kotlin так умеет:
или дажеval (x, y, z) = points3d[0]
Настоящих tuples, правда, нет (мотивация — не нужны), и примеры выше работают за счёт реализации методовfor ((k, v) in hashMap) { /* ... */ }
component1..N
.Stiver
15.09.2015 14:17>> А насчёт возврата нескольких значений — Kotlin так умеет:
Насколько я понял, все равно придется определять класс (да еще с определенными методами) и запихивать возвращаемые значения в него. Пример use case описал ниже, именно от этой работы хочется избавиться.h0tkey
15.09.2015 15:56Придётся, но, как в ветке ниже верно отметили, эта работа сведётся к одной строчке кода прямо перед объявлением метода. Для класса, помеченного аннотацией
data
, компилятор сам сгенерируетequals
,hashCode
,copy
иcomponent1..N
. Ну, ещё естьPair
иTriple
в стандартной библиотеке, если семантику полей не жалко. :)
burjui
15.09.2015 10:27+3В отличие от многих других синтаксических проблем Java, эта решается относительно легко при помощи коллекций или специальных классов. Даже если бы в Java были кортежи, я бы всё равно предпочёл использовать классы, т.к. кортежи привязывают тебя к определённому порядку значений и никак не спасают от путаницы, если хотя бы два значения в кортеже — одного типа. Короче говоря, уровень самодокументируемости у них почти равен нулю. Классы, напротив, завязаны на имена, поэтому совершенно не важно, какое поле у возвращаемого объекта вы хотите прочитать первым, и при обращении к одному из них вы точно будете знать, что получаете.
Посудите сами, зачем бы вам были нужны кортежи, будь в Java короткий и ясный синтаксис Kotlin:
grossws
15.09.2015 18:04В случае scala в простых случаях достаточно кортежей (а они там до 42-арных), а во всех остальных удобно использовать case class'ы, которые являются immutable, имеют простой синтаксис использования, удобное создание и декомпозицию (для pattern matching), реализуют разумные equals/hashCode/toString и т. д.
burjui
15.09.2015 11:04+3Ой, рука случайно дёрнулась и отправила комментарий раньше времени.
Так вот:
data class Point2D(val x: Int, val y: Int)
Если у вас синтаксис лаконичен, как в Kotlin, вам не составит труда написать ещё один data-класс в одну строчку, придумать для него осмысленное название и лишний раз поразмыслить, а действительно ли вам нужно возвращать несколько значений и законно ли это вообще — иметь у функции несколько выходных параметров, замаскированных под возвращаемое значение? Что такое вы собираетесь оттуда возвращать, что вам нужно делить это на части? Если возвращаемы значения независимы, не слишком ли много обязанностей на себя берёт функция? Если же они связаны, то ведь классы для этого и предназначены: связывать данные в отдельную монолитную сущность — как, например, Point2D. Кортежи позволяют делать такое:
y, x = getCursorPosition(); // упс, баг
Баг здесь появился только потому, что кто-то решил, что x и y независимы, но это не так: они часть единой сущности — точки в двумерном пространстве, и не имеют смысла по отдельности. В следующем коде так же не нужны кортежи:
file, err := os.Open("file.go")
Goты утверждают, что это якобы заставляет больше думать об обработке ошибок. Они серьёзно? Первым же делом после предъявления этого дутого аргумента они сами показывают, как доказать его несостоятельность:
file, _ := os.Open("file.go") // обработай это, чудила! ghetto style
А выкидывай они исключения вместо кодов ошибок, и не думать об обработке ошибок уже бы не вышло, при этом бизнес-логика была бы отделена от обработки ошибок, и можно было бы обрабатывать ошибки на выбранном уровне абстракции, а не делать это в каждой функции. В общем, спорная фича эти кортежи и прочие multiple return values.Stiver
15.09.2015 14:01>>>>а действительно ли вам нужно возвращать несколько значений и законно ли это вообще — иметь у функции несколько выходных параметров, замаскированных под возвращаемое значение? Что такое вы собираетесь оттуда возвращать, что вам нужно делить это на части? Если возвращаемы значения независимы, не слишком ли много обязанностей на себя берёт функция?
<<<<
Например функция получает граф, ищет определенный регион и возвращает входной узел и выходной. Узлы эти используются для дальнейших вычислений.
Теоретически можно придумать какой-то класс, объединить их в структуру и т.д. Но зачем? Ради одной-единственной передачи из функции? А если подобных функций у вас десятки? Что для кода с некоторой математикой/алгоритмикой скорее правило, чем исключение. Matlab без этой фичи вовсе не выжил бы, а в Яве приходится какими-нибудь Object[]{, } извращаться.burjui
15.09.2015 15:14В Java, пожалуй, это имело бы смысл. Но в Kotlin это всего одна лишняя строка:
data class Region(val entrance: Node, val exit: Node) // вот эта fun Graph.findSomeRegion(): Region = ... ... val region = graph.findSomeRegion()
и вам не надо помнить, что идёт первым — entrance или exit, даже если лень писать тесты. В этом конкретном случае очевидно, что entrance должен идти первым, но только для нас с вами, а вот компилятор скромно промолчит, если вы их перепутаете, т.к. оба значения имеют одинаковый тип.
Впрочем, возможно, что я перегибаю палку. Просто обычно я стараюсь переложить максимум работы на компилятор, потому что даже со 100% покрытием кода тестами нельзя гарантировать отсутствие багов, и лучше баги находить до того, как их найдут клиенты. Но находить баги при сборке для меня недостаточно, я стараюсь по возможности писать код так, чтобы исключать целые классы багов ещё до сборки (используя Option вместо null, например). Грань между программерской ленью и перфекционизмом довольно тонка :)
vintage
15.09.2015 16:03А если используется только один из них, а второй можно было бы и не вычислять? Как реализовать ленивое вычисление значений только если оно действительно нужно? Что делать, когда кроме входного узла и выходного нужно будет возвращать ещё пачку дополнительных данных? Пихать всё это в огромный кортеж? Подход яваскрипта тут лучше смотрится:
function findSomeRegion() { ... return { entrance : nodeEn , exit : nodeEx } } ... var region = findSomeRegion() // get all data as object var { nodeEn , nodeEx } = findSomeRegion() // destruct object to variables
burjui
15.09.2015 17:33Кстати, о лени: totallylazy. Имеются Sequence + генераторы, Option, а также filter, map/flatMap, fold/reduce и ещё кучка сверху. Без этой библиотеки и Retrolambda я бы уже повесился.
Stiver
15.09.2015 13:54.
bigfatbrowncat
18.09.2015 18:22У Kotlin, насколько мне удалось понять из документации, есть один очень серьезный дизайновый изъян (тот же, что и в C#). Там в архитектуру не заложены исключения, которые «трудно проигнорировать». Я говорю, разумеется, про checked exceptions. При этом объяснение в духе «глупые обезьяны всё равно выкрутятся, написав
catch (Exception e) { }
» совершенно несостоятельно. Мне нравится, что компилятор не дает мне «прозевать» IOException. А если совсем «приспичит», я этот IOException заверну в RuntimeException. Но 9 из 10 случаев, когда исключение не ловится — это когда оно тупо «прозёвывается».
Именно эта строгость Java и делает ее почти единственным приемлимым языком в гигантских Enterprise-проектах, как мне кажется.
Ой… нечаянно написал ответ.h0tkey
18.09.2015 18:50С проверяемыми исключениями далеко не всё однозначно, и в Java такой подход был скорее экспериментальным. В документации по поводу отсутствия проверяемых исключений в Kotlin ссылаются cюда, сюда и сюда.
bigfatbrowncat
19.09.2015 18:40Да-да. «один умный дядя сказал». Я читал эти статьи.
У меня критерий проверки — собственная шкура. И аргумент простой. Если кто-то говорит «это — ненужная фича — выпилить», а мне она помогает, я шлю этого «эксперта» вдаль, несмотря на его авторитет. Потому что эксперимент для меня ценнее любой теории.
К сожалению, запретить «проглатывать» исключения нельзя. Но то, что можно (и нужно) писать код, где каждый Exception отлавливается всегда, когда есть опасность, что он случится (хотя бы минимальная), и что я сам — ленивая сволочь, которая не напишет обработчик, если ее не пнуть, и что моё собственное честолюбие не позволит мне это исключение «проглотить», означает, что лично я с checked exceptions пишу более качественный код. И никто в обратном меня не убедит. А еще я верю, что я не один такой.
Я перед Java пару лет посвятил C#. Язык в большинстве возможностей мощнее и лучше, чем Java, но когда я увидел checked exceptions, то тут же пожалел, что там их не было. Кучу проблем в моем старом коде это бы решило еще на этапе его набора.
sborisov
Когда JetBrains введёт платную подписку на Kotlin? :)
Artem_zin
Лол, хоть я и не работаю в JetBrains, но очевидно, что, сам язык будет всегда бесплатным.
А вот тулинг (в чём JetBrains мастера), вполне может быть платным и это нормально.
vintage
Без бесплатных инструментов язык сейчас не продвинуть, если ты не Apple, конечно :-)
Artem_zin
На данный момент всё бесплатно (Kotlin Plugin для IntelliJ IDEA). Компилятор и интеграции к системам сборки, естественно, тоже + Open Source как язык, так и всё вокруг него.
vintage
А в чём подвох тогда? Не верится как-то что корпорация делает это просто из любви к программированию, без экономического обоснования :-)
Artem_zin
А в чём подвох бесплатности JRE и JDK? В обслуживании и тулзах. Здесь так же, и это прекрасно, для нас — пользователей.
В чём по вашему подвох Swift?
burjui
Им тоже надо на чём-то писать новые продукты — тот же CLion — и плагины. Устали ребята от бесконечного boilerplate, общей многословности Java и тупых компиляторов, которые заставляют писать
несмотря на то, что прекрасно знают, какой тип имеет выражение new Dummy(). Как Java лямбдами и всякими totallylazy не обмазывай, всё равно проступают угловатые контуры 90x.
burjui
Не раньше, чем плагин Kotlin для Gradle научится перекомпилировать изменившиеся .kt файлы. Шутки-шутками, но я бы даже из своего кармана заплатил, лишь бы на работе можно было перестать
беспокоитьсянасиловать жабу и начатьжитьписать на нормальном ЯП. Но не волнуйтесь, пока всё слишком сыро.