Архив метки: Kotlin

Kotlin-native & iOS

Дождались обновления kotlin-native v0.5. Теперь можно дергать кроссплатформенный код из Swift/Objective C. Подробнее здесь. Как Вы смотрите на появление курса Kotlin для iOS?

Автор: Roman Brovko

Kotlin. Часть 1. Введение

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

Kotlin

Логотип Kotlin, куда же без него

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

Kotlin — это очередной язык программирования со статической типизацией, который работает на той же виртуальной машине, что и Java. Делали его с 2010 года и не так давно он релизнулся с версией 1.0 (текущая 1.1). Совсем недавно Kotlin вошёл в топ 50 упоминаемых языков по версии индекса Tiobe (ссылка будет в конце).

Куда движется Kotlin?

Честно говоря, не понятно. Ребята, можно сказать, откусили большую часть рынка мобильной android разработки, теперь компилируются в javascript, Spring Framework уже наладили совместимость на бэкэнде, а в данный момент, параллельно с предыдущими активностями, пыхтит проект «Kotlin Native» (это LLVM бэкэнд, об этом в другой раз), который будет компилироваться под конкретную платформу и работать без виртуальной машины. Это напоминает наступление по всем фронтам (и бэкам).

Совместимость

Котлин на 100% совместим с Java. Что это означает? Вы можете взять свой Java проект и разбавить его качественным, концентрированно полезным кодом или наоборот — взять свой проект на Kotlin и написать туда немножко Java (ну как бы поностальгировать). Это одна из причин его большой популярности среди Android разработчиков. Применение первого вида совместимости понятно, вы можете использовать библиотеки типа guava или apache commons. А вот зачем нужно использование Kotlin из Java? Самое очевидное, что приходит на ум это постепенное переписывание старого кода на новый и, конечно, вы можете написать свою библиотеку на Kotlin скомпилировать её для совместимости, например, с Java 6 и использовать в вашем коде. Стоит сразу отметить, что максимальное количество преимуществ вы получаете, когда используете Kotlin вместо написания кода на Java 6.

Nullability

Разработчиков Kotlin охватила аллергия на NullPointerException и они, если и не избавились от NPE окончательно, то точно хорошо постарались, чтобы минимизировать их. Представьте, был у вас String и в нем могло лежать что угодно и строка, и null. Глядя на это JetBrains (да да, разработчики вашей любимой среды разработки еще и языки умеют делать), покачав головой добавили аппендикс вида «?» для типов переменных в которых может храниться null. Итого, в арсенале Kotlin разработчика кроме String появился String?. То же самое с остальными типами.

В Kotlin, по моим оценкам, брата NPE — KotlinNullPointerException (хитро не правда ли?) в 95% случаев можно встретить на стыке совместимости двух языков. Дело в том, что Java не знает ничего ни о каких String?, Int? и MyClass? по этому появляется специальный «платформенный тип». Вы не можете объявить переменную с этим типом сами, но выглядит он так «String!». Мне не хватает еще String% и String#, а вам?

Выведение типов

Пожалуй, лучшая фича Котлина это выведение типов (type inference). Давайте сравним:

Java 6

Map someMap = new HashMap();

Java 7, 8

Map someMap = new HashMap<>();

Kotlin

val someMap = HashMap()

Моё лицо, когда код стал короче на пару символов

В Котлине есть два ключевых слова без которых не обходится декларация ни одной переменной, это val и var. Наличие типа не обязательно, если сразу следует его декларация. Val и var означают immutable или mutable переменная соответственно. Т.е. на уровне компилятора у вас есть гарантии, что если вы написали val abc = «ok» в начале метода, то к концу метода никто не мог поменять это значение (никто никогда не использует final для переменных в Java, только зануды и разработчики библиотек).

Умное приведение типов

Приятная фишка Котлина — это «умное» приведение типов. Пример ниже:


val a: String? = "abc"
if(a != null) {
println(a.toLowerCase()) // если убрать if - код не скомпилируется
}

Компилятор «по умному» приводит String? к String и это снимает 85% оверхеда привносимого с Nullability.

Пара моментов, которых нас лишили

Здесь, естественно, не всё, а то, вы бы уснули от скуки.

Элвис бы в гробу перевернулся

Разработчики Kotlin дали нам замечательный Elvis оператор, который выглядит вот так ?: (поверните голову на -90 градусо

Kotlin. Часть 2. Незнакомые конструкции

Вот и наступил тот момент, когда вы познакомитесь с незнакомыми конструкциями в Kotlin. Эта статья является логическим продолжением Kotlin. Часть 1. Введение и её задача познакомить вас с конструкциями языка, которые могут быть крайне не привычны Java программисту. Нужно отметить, что если вы работали, например, с Groovy, то какие-то моменты вам могут быть до боли знакомы, но мы их всё равно рассмотрим. Те вещи, которые мы рассмотрели в предыдущей статье, затрагивать не будем. Если вы не прочитали предыдущую статью, рекомендую, для начала, прочитать её.

Современное лого Котлина

Для любителей проверить код

Чтобы создать простой проект на Kotlin — вам понадобится Intellij Idea Community Edition 2017 года. Скачать можно здесь. Если вы хотите полный туториал по хеллоу ворлду, то его можете прочитать и здесь, но я расскажу кратко как создать Kotlin проект с использованием системы сборки Gradle через Intellij Idea.

File -> New -> Project… -> Gradle -> Выбрать Kotlin (Java) и указать Project SDK, если не определен -> Next -> groupId и artifactId в данном случае не имеют значения, любые -> Next -> Ставим галку около Create directories… -> Next -> задаем нужный project name -> Finish

Готово! В папке src/main/kotlin вы можете писать на Kotlin.

First-class функции

В первую очередь то, что бросается в глаза это функции, которые можно писать вне классов. Типичный Hello World проект выглядит так:

fun main(args: Array) {
println("Hello World")
}

Это весь код, который нужно написать. Метод запускается без какого либо класса.

Нет ключевого слова new

Здесь всё просто, если вы хотите создать объект, вы не используете new, а просто вызываете конструктор, например:

val set = HashSet()

Классы Any, Array

В Котлине вы можете встретить несколько, а может быть и много, не знакомых классов. Краткое описание парочки таких ниже:

  • Any — местный аналог Object из которого вырезали кучу методов, например wait и notifyAll. Вы можете использовать Object, при глубокой необходимости, но часто этой проблемы нет.
  • Array — специальный класс, для работы с массивами. new MyClass[5] — конструкции в Котлине нет.

Mutable и Immutable коллекции

Вероятно, что вы помните из прошлой статьи, что переменные могут быть mutable и immutable благодаря var или val модификатору. Однако, если переменная является коллекцией, то на этот случай тоже есть свой хак. Интерфейсы List, Set, Map (Queue там нет, видимо за отсутствием смысла в неизменяемых очередях, почти как на почте) из пакета kotlin.collections — immutable, т.е. в них нет методов, которые могут изменить коллекцию, например, add для списка или put для мапы. Если вы хотите изменяемую коллекцию, то можете использовать MutableSet, MutableMap или MutableList. Таким образом вы можете декларировать можно ли менять коллекцию, которая возвращается из метода, еще на этапе компиляции, а не бросать exception во время выполнения.

Data класс

В Котлине, есть специальный модификатор, благодаря которому компилятором генерируются методы equals, hashcode, toString, copy и componentN.

data class User(val name: String)

Классы по умолчанию public final

В первую очередь, если вы джавист, создав новый класс вы привыкли видеть «public class …», здесь этого нет. Все классы по умолчанию public и final. Для того чтобы наследоваться от класса, вам нужно явно прописать модификатор open или abstract (в этом случае open подразумевается и не требуется).

Void -> Unit

Когда ваш метод не возвращает значение в Java вы пишите void в декларации метода, в Kotlin вы не пишите ничего, но в голове держите, что возвращается Unit. Это эдакая альтернатива.

Класс Nothing

Этот класс не имеет инстансов и его объявление в декларации метода как возвращаемого типа означает, что метод всегда бросает exception.

Операторы

Появился модификатор operator благодаря которому, вы можете делать собственные реализации для +, — и так далее.

Лямбды

Объявление самого простого варианта лямбды выглядит следующим образом:

() -> Unit

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

Что я специально пропустил

Следующие вопросы я специально оставил для вашего изучения:

  • Sealed class;
  • как метод comp
    onentN улучшает читаемость кода;
  • Generics — это очень специфичная тема и для неё нужно отдельную статью;
  • модификатор infix и lambda with receiver будут рассмотрены в последней статье цикла;
  • местная альтернатива Stream API в Java — будет рассмотрена в следующей статье;

И, обязательно, я что-то пропустил не специально.

Заключение

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

Автор: Roman Brovko

Kotlin. Часть 3. Мигрируем из Java

Ранее вышло две статьи по языку Kotlin. В первой статье мы с вами познакомились с языком, а во второй рассмотрели популярные конструкции, которые могут быть не очевидны для типового Java разработчика.

Цели миграции

Прежде чем внедрять Kotlin в ваш проект давайте определимся каким целям вы следуете. Естественно вы хотите улучшить качество кода, ускорить написание и поддержку части или всей системы. Прежде всего, следует определить о каких масштабах идет речь:

  1. Вас интересуют только тесты.
  2. Вас интересует только новая функциональность или часть старого кода.
  3. Вас интересует полная замена старого кода и дальнейшая разработка.

Чем ниже позиция, тем больше масштаб трагедии.

Если вас интересует пункт 1, то данная статья вам не нужна, для вас будет 4 и 5 часть цикла о Kotlin.

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

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

Картина: Котлин захватывает умы мобильных разработчиков

Java + Kotlin

Давайте разберемся как же совместно работают Java и Kotlin с технической стороны. В чем мы уверены? В виртуальной машине! Есть байт-код, который будет исполняться на виртуальной машине Java. Байт-код мы получаем в результате компиляции Java и Kotlin. На итоговой машине может стоять любая версия JVM, а нам подойдет 6, 7 или 8, т.к. Kotlin может быть скомпилирован под одну из них с незначительными ограничениями. Как вы, возможно, знаете, файлы с расширением .java, которые содержат исходный код, компилируются с помощью javac в файлы .class, которые содержат байт-код. Аналогично в случае Kotlin. Файлы с исходным кодом имеют расширение .kt, а файлы с байт-кодом также .class. Компиляция происходит с помощью компилятора kotlinc. На картинке ниже изображен упрощенный процесс сборки приложения с Kotlin.

Упрощенный процесс сборки приложения

Как видно выше приложение с Kotlin требует Kotlin Runtime библиотеки. Благодаря ним в приложении становятся доступными стандартные Kotlin классы и расширения.

Полная совместимость означает, что вы можете не только использовать классы Java, но и наследоваться от них, реализовывать интерфейсы, применять Java аннотации в своем Kotlin коде и т.д. Более того, из Java можно вызывать Kotlin код в естественном виде, никаких спец. хаков, ничего лишнего.

На случай, более привычного использования Kotlin библиотек в Java, есть специальные средства для того, чтобы минимизировать отличия Kotlin от Java после сборки. Например, first-class функции на самом деле оборачиваются в сгенерированные классы, но названия этих классов менее звучные, чем, например, StringUtils. Для того, чтобы из Java кода имена сгенерированных Kotlin классов-оберток были привычнее, вы можете использовать конструкцию

@file:JvmName("SomeUtils")

fun someFunc(param: String?) {}

Такой код в Java будет выглядеть как вызов

SomeUtils.someFunc("abc"); 

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

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

Инструменты миграции

Вместе с языком Kotlin в цикл разработки входит конвертер из Java в Kotlin. Сразу обратите внимание, что обратной конвертации нет. Хорошо, если у вас есть система контроля версий, например, Git, и вы сможете отк
атить изменения, если они вас не устроят. Конвертировать можно из меню Code -> Convert Java File to Kotlin File или используйте shortcut Сtrl + Alt + Shift + K.

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

Просмотр байт-кода

В плагин для Kotlin входит функциональность просмотра получающегося байт кода. Для того чтобы его увидеть достаточно в меню Tools -> Kotlin выбрать Show Kotlin Byte code.

Заглянем за кулисы

Давайте посмотрим на особенности байт-кода Kotlin. Там мы можем найти кое что интересное для понимания принципов языка.

Такая замечательная вещь как контроль Nullability привносит с собой не значительный оверхед в виде явной проверки на null. Для примера я приведу пустой метод, который ничего не делает, но требует значение параметра не равный null (напомню, что в случае nullable параметра в Kotlin используется String?).

fun someFunc(param: String) {} 

Для этого метода, кроме RETURN команды, в байт-коде мы увидим явную проверку на null:

INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V

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

fun someFunc(param: String?) {}

из байт кода, естественно, исчезает проверка, но всегда генерируется аннотация Nullable или NotNull (это помогает среде разработки подсвечивать опасное использование переменных, которые могут быть null)

  @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0

Особенности конвертации

Конвертер не стоит на месте и силами разработчиков языка исправляются всё больше ошибок. Наиболее мажорные из них исправлены к версии 1.1. Тем не менее, конвертированный код не идеален и ниже рассмотрим конвертацию лямбд из Java в Kotlin, а также Stream Api.

Отличие лямбд в Kotlin и Java

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

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

Например, код метода main

fun main(args: Array) {
invokeLambda {
println("Hello world")
}
}

inline fun invokeLambda(lambda: () -> Unit) {
lambda()
}

в результате имеет байт код схожий, но не в точности, с

fun main(args: Array) {
println("Hello world")
}

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

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

Рассмотрим результат конвертации лямбды. В Java, обычно, для типичных лямбд используются такие функциональные интерфейсы как Runnable, Supplier, Function и т.д. В Kotlin же, как вы уже знаете, тип лямбды декларируется как (ParamsTypes) -> ReturnType. Изначально мы имеем следующий код:

public class Example {
public static void main(String[] args) {
invokeLambda(() -> {
System.out.println("Hello world");
});
}
private static void invokeLambda(Runnable lambda) {
lambda.run();
}
}

В результате конвертации мы получаем Kotlin код:

object Example {
@JvmStatic fun main(args: Array) {
invokeLambda { println("Hello world") }
}
private fun invokeLambda(lambda: Runnable) {
lambda.run()
}
}

Ключевое слово object — обозначает своего рода singleton, т.к. у класса нет контекста (полей класса), то нет никакого смысла создавать для него новые инстансы.

Для того чтобы метод main из Java был представлен как статический (в Kotlin нет статических методов, для этих целей обычно служит companion object) используется аннотация @JvmStatic.

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

fun main(args: Array) = invokeLambda { println("Hello world") }

private fun invokeLambda(lambda: () -> Unit) = lambda()

В версии Kotlin 1.1 всё конвертируется отлично, хотя в прошлых версиях была проблема с конвертацией лямбд.

Stream Api

Как вы знаете, с появлением Java 8 в арсенале разработчика появился мощная возможность. Имя ей Stream Api. Задача, которая решается с его пом
ощью — это обработка коллекции данных в функциональном стиле. При этом код в Java выглядит компактным и хорошо структурированным. В Stream Api есть терминальные операции — это завершающие операции, при достижении которых начинается обработка описанного стрима (forEach, collect, sum и т.д.). Важный момент, стримы в Java исключительно lazy, т.е. выполняются только при достижении терминальной операции, а если её нет, то стрим просто ничего не сделает.

В Kotlin, есть аналог Stream Api, но в отличии от Java есть еще и не lazy обработка. Она доступна прямо на уровне коллекции, например

val incrementedCollection = someIntCollection.map { it + 1 }

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

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

Итак, давайте конвертируем Java stream в Kotlin. Пусть у нас есть исходный код, который должен собирать фразу «hello world !» на Java вот таким изощренным способом.

List words = Arrays.asList("Hello", "my", "world", "world", "!");
String resultString = words.stream()
.filter((word) -> !Objects.equals("my", word))
.map(String::toLowerCase)
.distinct()
.collect(Collectors.joining(" "));
System.out.println(resultString);

Воспользуемся конвертером

Бэм! И мы получили не компилируемый код. Как я писал, конвертер не идеален.

val words = Arrays.asList("Hello", "my", "world", "world", "!")
val resultString = words.stream()
.filter { word -> "my" != word }
.map(Function { it.toLowerCase() })
.distinct()
.collect(Collectors.joining(" "))
println(resultString)

Для начала заставим его компилироваться. Для этого подкорректируем его и получим уже компилируемый код

val words = Arrays.asList("Hello", "my", "world", "world", "!")
val resultString = words.stream()
.filter { word -> "my" != word }
.map { it.toLowerCase() }
.distinct()
.collect(Collectors.joining(" "))
println(resultString)

В принципе, задача решается уже верно, но давайте перепишем на нативное использование Kotlin без привязки к Java. Ради красоты и читаемости.

    val words = listOf("Hello", "my", "world", "world", "!")
val resultString = words.asSequence()
.filterNot { it == "my" }
.map(String::toLowerCase)
.distinct()
.joinToString(separator = " ")
println(resultString)

Готово! Понимаю, что использование filterNot, конечно, на любителя, но в целом мы имеем отличный, читаемый, компилируемый ленивый поцессинг коллекции. А если убрать asSequence, то процессинг перестанет быть ленивым. Оператор «==» в Kotlin это equals в Java, т.е. в Kotlin строки можно всегда сравнивать через «==».

Модификатор internal

Наконец, в Kotlin есть модификатор internal использование его обозначает, что метод/класс/поле доступно к использованию только внутри модуля (jar-ника). Если вы использовали для класса A этот модификатор, запаковали его и подключили как зависимость, то этого класса в зависимом проекте вы не найдете. Но как вы понимаете в Java нет ничего подобного, а всё реализуется на виртуальной Java машине. Решение оказалось не сложным. При компиляции internal компоненты языка проходят своего рода обфускацию, т.е. они переименовываются страшным образом и использовать такие поля/методы/классы из Java кода не очень хочется.

Заключение

В этой статье мы рассмотрели как работают вместе Java и Kotlin. Какие проблемы могут быть и как они решаются. Я постарался раскрыть эту тему наиболее полно, но если у вас остались какие-либо вопросы, то вы можете их задать.

Автор: Roman Brovko

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. Каждый бот имеет свой ключик, который выдается при создании бота для у