Архив автора: admin

Kotlin. Часть 4. Неловкие моменты

По заголовку становится ясно, что мы проделали уже очень большой путь. В серии Kotlin вышло целых три статьи из пяти (Введение, Незнакомые конструкции, Мигрируем из Java) и это четвертая в которой я хотел бы рассказать вам о том, как не выстрелить себе в ногу, применяя этот язык и как понять причину странного поведения программы. В следующей заключительной статье мы будем обсуждать как реализовать свой DSL (проблемно-ориентированный язык) с помощью Kotlin на примере библиотеки для создания Telegram ботов.

Если вдруг вам не знакомо что-то, что я использую без объяснений, то рекомендую прочитать предыдущие статьи цикла

Типичный рабочий стол Kotlin разработчика

Неизвестный синтаксис:

Если последним параметром функции в Kotlin передается лямбда, то её можно вынести за скобки, если это единственный параметр, то скобки можно не писать, очевидный пример — forEach

Начнем мы с самого популярного места: inline функции.

Inline функции с return

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

Inline (встраиваемые) функции таят в себе небольшие, но лакомые для ошибок моменты о которых должен знать каждый уважающий себя Kotlin разработчик. Такие функции будут встроены в место вызова и код лямбд, передаваемых в них, будет встроен тоже. В такой ситуации нужно понимать как правильно манипулировать ключевым словом return. Давайте рассмотрим следующий пример:

fun main(args: Array) {
listOf("three", "two", "one").forEach {
if(it == "one") {
return
}
println(it)
}
println("boom!")
}

Как вы думаете что увидит пользователь?

Ответ тру Java разработчика — «three», «two», «boom!»

Ответ тру Kotlin разработчика — «three», «two»

И действительно, при запуске такого метода в Kotlin последняя строчка метода выполнена не будет. Причина этому функция forEach, а вернее её модификатор — inline.

Что делает return в Kotlin? Простой вопрос и ответ соответствующий: Return либо возвращает из функции значение, либо как в этом примере прерывает её выполнение. Только вот если встроить функцию, то формально никакого вызова в этом месте уже не будет. Почему в Java не так? Как вы знаете в Java каждая лямбда это инстанс анонимного класса, у которого определен метод, в Kotlin это не всегда так. Естественно, так как мы работаем в рамках JVM, то другой реализации добиться довольно трудно, да и лично я никогда не понимал «а как иначе?». Есть кусок кода и его нужно хранить. Также мы видели альтернативный вариант это передача ссылки на метод, который в себе содержит нужный код, однако, я решил заглянуть «под капот», подготовил вот такой пример:

fun example() {
printUUID(::uuidGenerator)
}
fun printUUID(supplier: () -> UUID) {
println(supplier())
}
fun uuidGenerator() : UUID {
return UUID.randomUUID()
}

Для передачи ссылки на first-class функции используется «::»

И что я вижу в байт коде Kotlin?

GETSTATIC ExampleKotlinKt$example$1.INSTANCE : LExampleKotlinKt$example$1;
CHECKCAST kotlin/jvm/functions/Function0
INVOKESTATIC ExampleKotlinKt.printUUID (Lkotlin/jvm/functions/Function0;)V

Если верить спецификации виртуальной машины, то мы с вами являемся свидетелями единственного инстанса (ExampleKotlinKt$example$1.INSTANCE) анонимного класса, а значит и здесь без них никак.

Давайте вернемся к первому примеру. Функция forEach — это встраиваемая функция. Существует понятие, которое применимо в Kotlin — non-local return. Именно его мы и наблюдаем. Простыми словами, non-local return, это такой return, который способен прервать выполнение функции, которая окружает встраиваемую. Для того чтобы получить ожидаемое поведение нам следует воспользоваться return к метке вот так

fun main(args: Array) {
listOf("three", "two", "one").forEach {
if(it == "one") {
return@forEach
}
println(it)
}
println("boom!")
}

«three»,»two»,»boom!»

noinline

Иногда не нужно встраивать все параметры, в такой ситуации помогает модификатор на параметре функции noinline. Рассмотрим следующий пример:

inline fun someFun(lambda: () -> Unit) {
lambda()
}
fun main(args: Array ) {
someFun {
return
}
println("boom!")
}

Путь изначально у нас есть inline функция someFun у которой все параметры встраиваются (по умолчанию). Что если мы захотим не встраивать передаваемую лямбду? Пример ниже не компилируется, т.к. non-local возврат из лямбды, которая точно не будет встроена невозможен

inline fun someFun(noinline lambda: () -> Unit) {
lambda()
}

fun main(args: Array) {
someFun {
return
}
println("boom!")
}

Как исправить? Очень просто! Добавляет return к метке. Это мы уже умеем. В такой ситуации не получится сделать non-local return, пожалуй, это и хорошо

inline fun someFun(noinline lambda: () -> Unit) {
lambda()
}

fun main(args: Array) {
someFun {
return@someFun
}
println("boom!")
}

Рассмотрим еще один модификатор, который используется для параметров inline функций — crossinline

crossinline

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

class Store(val lambda: () -> Unit)
inline fun someFun(lambda: () -> Unit) {
Store {
lambda()
}
lambda()
}
fun main(args: Array) {
someFun {
return
}
println("boom!")
}

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

Для того чтобы компиляция заработала нужно воспользоваться модификатором crossinline. В этом случае компилятор будет запрещать non-local return в передаваемых лямбдах и при этом инлайнить эти лямбды там, где это возможно внутри someFun. Теперь мы будем использовать только return к метке, внутри передаваемых crossinline параметров. Код ниже становится компилируемым, при этом лямбда будет встроена в контексте функции, которая, в свою очередь, тоже будет встроена.

class Store(val lambda: () -> Unit)

inline fun someFun(crossinline lambda: () -> Unit) {
Store {
lambda()
}
lambda()
}

fun main(args: Array) {
someFun {
return@someFun
}
}

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

class Store(val lambda: () -> Unit)

inline fun someFun(inlineLambda: () -> Unit,
noinline noinlineLambda: () -> Unit,
crossinline crossinlineLambda: () -> Unit) {
Store {
//inlineLambda cannot be used
noinlineLambda() //not inlined
crossinlineLambda() //not inlined
}
inlineLambda() //inlined
noinlineLambda() // not inlined
crossinlineLambda() //inlined
}

fun main(args: Array) {
someFun({
println("Print 1")
return //it is non-local return and it is ok for inline lambda
}, {
println("Print 1")
return@someFun //non-local return is not compiled here
}) {
println("Print 3")
return@someFun //non-local return is not compiled here
}
}

Вот ссылка на Gist, чтобы сохранить себе.

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

Наиболее насыщенная часть на этом заканчивается и «неловкие моменты», которые вы встретите дальше будут значительно проще в понимании.

Переопределенные операторы

В Kotlin вы можете переопределить операторы. Отнеситесь к этому внимательно, т.к. по ошибке можно не правильно понять принцип действия того или иного оператора, например «==». В Java «==» означает ссылочное равенство, в Kotlin всё чуть более сложнее. Оператор «==» соответствует методу equals, т.е. проверяет структурное равенство, соответственно в примере выше (с forEach) «==» для сравнения строк — правильный синтаксис. Для того, чтобы получить аналогичную Java проверку на ссылочное равенство в Kotlin используют «===». Посмотрите полный перечень операторов Kotlin и вы обнаружите, что проверка на вхождение в коллекцию (in) это тоже оператор связанный с методом contains. Вы можете перейти к реализации in и убедиться в этом сами.

val c = ArrayList()
val a = 1
println(a in c)

Вложенные лямбды

Иногда, например при построении DSL, когда вы используете вложенные лямбды может сложиться неловкая ситуация. Очень важно следить за контекстом, т.к. this это не всегда то, что вы мож
ете подумать. Давайте посмотрим на пример:

fun main(args: Array) {
Context().apply {
println(this)
innerContext {
println(this)
doSomething()
}
}
}
class Context {
private val innerContext: InnerContext = InnerContext()

fun innerContext(init: InnerContext.() -> Unit) {
innerContext.init()
}
fun doSomething() {
println("Outer context")
}
}
class InnerContext {
fun doSomething() {
println("Inner context")
}
}

В примере есть функция main и она создает контекст и у него вызывает функцию apply. Всё что вам нужно знать сейчас об этой функции, так это то, что внутри неё this — это тот объект на котором она вызвана. Как этого достичь я расскажу в следующей заключительной статье основного цикла Kotlin.

Итак, мы видим, что внутри apply вызывается функция innerContext в которую передается лямбда с обработчиком (также работает apply, но об этом в 5й части). Фактически, метод служит для запуска лямбды и всё. Как вы видите в метод main мы передали лямбду и что же вернет println? Не буду томить, внутри этой функции он напечатает совсем другой this. Давайте посмотрим на вывод:

Context@511d50c0 //println(this) внутри apply
InnerContext@2b193f2d //println(this) внутри innerContext
Inner context //результат doSomething

Как видно из вывода, что при смене контекста меняется не только this, но и вызываемые методы (doSomething). Это очень важно, по этому следите за контекстом, чтобы не допускать таких ошибок.

Метод который возвращает лямбду

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

fun printA() = { println("a")}

В этом коде есть одна большая проблема, он возвращает лямбду, а не выполняет печать, т.е. всё что после «=» — лямбда, а «=» это присваивание её как возвращаемого значения функции. Следовательно печать может быть достигнута только так printA()(). Жутко, не правда ли? Не возвращайте лямбды в совокупности с сокращенной записью метода.

Smart cast

Не забывайте про smart cast. Компилятор после вашей проверки на null внутри if конструкции приводит String? к String, то есть внутри стейтмента if он не требует от вас явных проверок и в этом примере «!!» не нужно.

val a : String? = "a"
if(a != null) {
print(a!!.isBlank())
}

Методы вызываемые на null

Удивительно, но в Kotlin сделали методы, которые можно вызывать даже при null по ссылке у которой вызывают метод, такой метод вы можете объявить для на nullable типа в качестве extention функции (о них поговорим в последней статье). Давайте посмотрим на следующий пример.

val s: String? = null
println(s.isNullOrEmpty())

Не смотря на то, что метод вызывается на строке в которой может быть null, компилятор разрешает это делать, т.к. в декларации метода прописано, что для этого метода this может быть равен null и безопасность nullability обеспечивается уже на другом уровне — внутри метода. Декларируется метод, например так:

fun CharSequence?.isNullOrEmpty(): Boolean

Отладка Kotlin + Java

На данный момент известно одно, отладка Kotlin + Java совсем немного хромает, не значительно. В принципе на уровне приложения всё ок, но вот я захотел поставить точку останова внутри println и этого сделать уже не смогу, вернее смогу, но она будет проигнорирована, хотя глубже в System.out.println (который вызывается в println) уже работает. Неведомые вещи творятся, еще в интернете можно встретить цитирование вот этих слов:

The currently selected Java debugger doesn't support breakpoints 
of type 'Kotlin Line Breakpoints'. As a result, these breakpoints
will not be hit. The debugger selection can be modified in the run
configuration dialog.

Сам глубоко не копал, об этом можно будет поговорить отдельно, но будьте бдительны.

«Переопределяющая» extention function

В Kotlin есть понятие extention функции. Это функция которая может быть добавлена в тот или иной класс без изменения кода этого класса. В Java такая функция будет выглядеть как статический метод, а в Kotlin работает как часть синтаксиса. Важно понимать, что такие функции не «встраиваются» в класс буквально, они лишь работают с его публичным API. Так например есть много функций для обработки Java коллекций при этом сами классы коллекций не тронуты. Но что произойдет, если такая функция перекроет существующую? Давайте взглянем на код.

class Abc {
fun someFun() {
println("Source fun")
}
}
fun Abc.someFun() {
println("Updated fun")
}

fun Abc.someFun — так выглядит определение extention функции. Мы указываем на какой класс она нацелена (здесь может быть даже дженерик тип), а з

Kotlin. Часть 5. Пишем DSL

Вот и пришла пора для заключительной статьи в основном цикле о Kotlin. Что это значит? Пользуясь знаниями из предыдущих статей (Введение, Незнакомые конструкции, Мигрируем из Java, Неловкие моменты) ниже мы с вами напишем собственный DSL (domain-specific language), обсудим что это такое и чем Kotlin, как язык, способствует написанию предметно-ориентированных языков, а также как это упрощает разработчикам жизнь, я расскажу о собственном опыте в разработке DSL и о проблемах, которые возникают. Статья может показаться довольно большой, так и есть, я постарался уместить в неё основной концентрат собственных знаний о построении DSL в Kotlin.

Давай же, Kotlin! На тебя все смотрят!

Что такое DSL?

Языки программирования можно разделить на 2 типа: универсальные языки (general-purpose programming language) и предметно-ориентированные (domain-specific language). Популярные примеры DSL это SQL или регулярные выражения. Язык уменьшает объем функциональности который, он дает, при этом он способен эффективно решать определенную проблему. В том числе это способ описать программу не в императивном стиле, когда вы говорите как нужно получить результат в универсальны языках, а в декларативном, когда вы говорите что вы хотите получить. Пусть у вас есть стандартный процесс выполнения, который иногда может меняться, дорабатываться, но в целом вы хотите подстраивать его под разные данные и формат результата. Создавая DSL вы делаете гибкий инструмент для решения различных задач из одной предметной области при этом не задумываетесь об оптимизации. Это некоторое API, виртуозно пользуясь которым, вы можете сильно упростить себе жизнь и долгосрочную поддержку системы.

«Чистое API»

«Чистым» называется API, которое требует минимальных действий для получения результата, никакой мишуры и фантиков. Давайте рассмотрим, что нам дает Kotlin для построения такого API.

На пути к совершенству

Давайте перечислим основные преимущества Kotlin, которые позволяют достаточно чисто писать на этом языке и доступны для построения собственного DSL. Ниже представлена табличка в которой перечислены основные улучшения синтаксиса языка, которые стоит использовать

Преимущества Kotlin, которыми стоит пользоваться

Благодаря этим возможностям вы способны писать код чище, избавиться от множества вызовов методов и при этом сделать разработку еще более приятным занятием («куда уж приятнее?» — спросите вы). Мне понравилось сравнение из книжки, что в натуральных языках, например, в английском, предложения построены из слов и граматические правила управляют тем как нужно объединять слова друг с другом. Аналогично в DSL, одна операция может быть сложена из нескольких вызовов методов, а проверка типов обеспечит гарантию, что конструкция имеет смысл.

Оказывается, что довольно трудно совместить одновременно универсальный язык и DSL, стереть границу которая их разделяет. Прикрутить что-то к чему-то можно всегда, но как это сделать, чтобы работа с таким DSL была максимально простой и нативной? Давайте обсудим «внутренние DSL» и как они стирают интеграцию.

Внутренний DSL

Проще всего понять что такое «внутренний» DSL на примере. Вот у вас есть реляционная база данных и мы хотим выбрать из неё определенные записи, как мы это делаем? Очевидно, SQL. У нас будет select запрос в котором мы укажем что и как нужно достать. В перевес этому мы можем взять фреймворк Exposed на Kotlin (не то чтобы я сторонник миллионов оберток, но это хороший пример). Exposed — это работа с базой данных в sql подобном стиле прямо из кода. Больше подробностей по фреймворку по ссылке в конце статьи.

Пример DSL

Предлагаю посмотреть, а ради чего всё это. Я приведу пример из практики. Библиотека Clabo для создания телеграм ботов в декларативном стиле, демонстрирует возможности, которые дает Kotlin для построения DSL. Каждый бот имеет свой ключик, который выдается при создании бота для у

Русификатор для текстового редактора Atom

Русификатор для текстового редактора Atom

Русификатор для текстового редактора Atom

Друзья, ищу русификатор для текстового редактора Atom (очень классная штука, если кто не знает). Возможно в будущем вообще перейду с NotePad++ на Atom, но для этого надо его хорошо знать. Не просто главные функции, которые «и любой желающий может прочитать на английском», а хочется пройтись по меню и хорошо узнать все возможности этого зверя.

Кто подскажет?

Почему не получается похудеть?

Чем дальше заходит научный прогресс, тем больше новых и неожиданных вещей мы узнаем. Диетология не исключение. Как выясняется в процессе исследований, даже незначительные факторы могут сильно влиять на процесс похудения. Расскажем об этих факторах.
Медицинские препараты
Имеются в виду препараты, которые довольно серьезны, например, антидепрессанты, препараты для диабетиков, противовоспалительные препараты, лекарства против эпилепсии, и т.д. Если вы являетесь человеком, который по медицинским показаниям принимает подобные препараты, то это может вам мешать в процессе похудения. Даже если вы полностью придерживаетесь правильного питания и подвижного образа жизни.
Молочные продукты
В молочных продуктах содержится большое количество сывороточного протеина. В кисломолочных продуктах его особенно много. На основе этого протеина делается спортивное питание для набора мышечной массы. Последние исследования выявили, что этот белок повышает у некоторых людей уровень инсулина, это и мешает потере веса. Попробуйте исключить все молочные продукты на месяц, и если ваш вес начнет уменьшаться (при соблюдении питания и физической нагрузки), значит вы как раз относитесь к этим сверхчувствительным людям, и впредь надо относиться к молочным продуктам с осторожностью.
Дефицит железа и гипотиреоз
О наличие этих заболеваний многие не догадываются, особенно если это начальная стадия заболевания. У человека наблюдается набор веса, угнетенное настроение, упадок сил. Выявить эти болезни с первого раза не всегда удается даже врачам, следует сдать нужные анализы. Эти диагнозы чаще встречаются у женщин от 35 лет.
Большое количество углеводов
Это часто встречающееся явление. Углеводы – коварный элемент, только маленькая их часть гарантированно не отложится в жир, а вот остальное их количество зависит от вашего образа жизни. Если вы ведете подвижный образ жизни или являетесь спортсменом, то лишние углеводы будут израсходованы. И наоборот, если ваш образ жизни малоподвижен, то углеводы будут откладываться в жир.






Альфаро бирюзовый

Альфаро бирюзовый

Альфаро ножевидный – стайная рыбка, содержать их рекомендуется в группе из 5-10 особей или больше. Объем аквариума для группы – 100-150 л. При наличии соседей объем следует увеличить.

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

Оптимальная температура воды для бирюзовых альфаро – 23-28°С, жесткость – до 15°dH, кислотность – 6,5-8 рН. Данный вид рыбок вполне благосклонно относится к подсоленной воде. Подмены воды производят еженедельно, заменяя примерно треть объема.

Брекет-система Damon Q – с ней я чувствую себя уверенно

 

Брекет-система Damon Q – с ней я чувствую себя уверенно

 

 

Сколько себя помню, я всегда стеснялась своей улыбки и вообще была недовольна своей внешностью из-за ужасно неровных зубов. Они же стали и причиной моей закомплексованности. В юности я как-то особо не задумывалась об исправлении этой проблемы — думала, что со временем зубы сами выровняются. Но время шло, а проблема с зубами стала еще хуже… И вот, когда мне исполнилось 26, я решила, что пора что-то менять. Было решено поставить брекет-систему. Перед тем, как сделать это, перечитала статьи на интересующую тему в интернете, отзывы, общалась на тематических форумах. Также посетила стоматолога в местной поликлинике, который направил меня к ортодонту в областной центр.