Идёт мобильный разработчик по лесу, видит — Котлин горит. Сел в Котлин и сгорел

Мир сходит с ума. Говорят, все новые мобильные проекты на Андроиде пишут исключительно на Котлине. В наше время очень опасно не учиться новым технологиям. Вначале твои знания устаревают, ты вылетаешь с работы, живешь у теплотрассы, дерёшься с бомжами за еду и умираешь в безвестности, так и не выучив функционального программирования. Поэтому я отправился на Курсеру изучать курс Kotlin for Java Developers и начал читать книжку (привет, abreslav, yole), поспрашивал друзей сами знаете откуда и вернулся назад с некой пустотой в душе. Помогите Олегу-путешественнику найти смысл в Котлине!

ua41wbfvcsn2uwhlqabj5pakpic.png

●  В Java ты чаще понимаешь по узкому контексту, что происходит. a = b — запись в поле или локал, a[1] = 2 — запись в массив. В Котлине за любым простым выражением может стоять сколь угодно сложный код из-за всяких умностей вроде перегрузки. Без IDE ничего не поймёшь. А IDE плохо, когда ты едешь в поезде и видишь, что свинговый жабоинтерфейс высасывает из ноутбука батарейку как вампир.

●  Котлин даёт одинаковый API для коллекций и сиквенсов, из-за чего люди злоупотребляют цепочками map/filter на коллекциях, создавая кучу промежуточных неленивых копий. Стримы в джаве специально введены для различия между ленивой и неленивой коллекцией. Да, есть инспекция в IDE для этого — потому что инспекции призваны исправлять недостатки языков.

●  Кстати, об IDE. Насколько хороша поддержка Kotlin в IntelliJ IDEA? Она действительно лучше, чем для Java? Есть большие сомнения. Может быть, кому-то из JB хватит духу проадвокатировать по данному вопросу.

●  Котлин форсит использование it, что приводит к нечитаемому коду. Что-нибудь типа seq.map { it -> foo(it, 1); }.map { it -> bar(it, 2); }.filter { it -> it.getBaz() > 0; }. Что это вообще было? Имена переменным даны не зря! А тут получается монолог вроде «Возьмём это, прикрутим к нему то, потом его закрутим и если оно стало больше того, то наденем сверху шарнир».


●  Цепочки вроде ?.let { foo(it); }?.let { bar(it); } — это вообще ад и должны быть запрещены в декларации о правах человека. И это считается идиоматично, Карл. В отличие от нормального if. Читать такой код невозможно.

●  От интеропа с джавой кровь идёт из глаз. А тут всякие JvmStatic и JvmName, и код превращается в цирк с конями.

Например, вот у нас есть такое:

class C {
    companion object {
        @JvmStatic fun foo() {}
        fun bar() {}
    }
}

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


  • C.foo(); — работает
  • C.bar(); — синтаксическая ошибка, ибо метод не статический
  • C.Companion.foo(); — остается метод экземпляра
  • C.Companion.bar(); — единственный правильный способ

Оправились от красоты решения? Окей, пошли дальше. Теперь вы готовы понять и принять тот факт, что, например, нельзя одновременно объявить два таких метода:

fun List.filterValid(): List
fun List.filterValid(): List

Ведь их сигнатуры на уровне JVM совпадают: filterValid(Ljava/util/List;)Ljava/util/List;

Поэтому нужно подпихнуть специальный костыль:

fun List.filterValid(): List

@JvmName("filterValidInt")
fun List.filterValid(): List

А как вам такое: в Kotlin нет checked exceptions. А в JVM-реальности они есть. Отряд специального назначения «Боевые протезы» имеет честь представить новый самоходный костыль @Throws:

@Throws(IOException::class)
fun foo() {
    throw IOException()
}

Можно долго рассуждать, что «джависты постоянно ноют, что тут всё не как в джаве». Но если вот это красиво, то что тогда ужасно?

В общем, рекомендуется открыть статью Java-to-Kotlin Interop и своими глазами посмотреть, как это выглядит.

●  Автоматические геттеры/сеттеры с добавлением английского слова get и первой буквой проперти в большом регистре (видимо, в локали ENGLISH? Ведь регистр букв системно-зависим) — это страшно.

import java.util.Calendar
fun calendarDemo() {
    val calendar = Calendar.getInstance()
    if (calendar.firstDayOfWeek == Calendar.SUNDAY) {  // call getFirstDayOfWeek()
        calendar.firstDayOfWeek = Calendar.MONDAY      // call setFirstDayOfWeek()
    }
    if (!calendar.isLenient) {                         // call isLenient()
        calendar.isLenient = true                      // call setLenient()
    }
}

●  Экстеншн-методы загрязняют публичный интерфейс такими вещами, о которых автор и подумать боялся.

Так как этой фичи нет в джаве, поясню. Можно написать любой метод, слева поставить имя «принимающего класса», и всё — он расширен. Давайте расширим MutableList функцией swap:

fun MutableList.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' относится к листу
    this[index1] = this[index2]
    this[index2] = tmp
}

val lst = mutableListOf(1, 2, 3)
lst.swap(0, 2) // 'this' внтури 'swap()' будет иметь значение 'lst'

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

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

●  Библиотека местами не продумана. Например, reduce.

Вот как выглядит reduce:

listOf(1, 2, 3).reduce { sum, element -> sum + element } == 6

Там есть только форма с identity (fold), но она не всегда применима.

listOf(1, 2, 3).fold(0) { sum, element -> sum + element } == 6

Кстати, почему Хабр подсвечивает эти две строчки по-разному? Аааа, уже неважно.

По сути, fold и reduce делают одно и то же, но fold требует определённого начального значения, а reduce использует в качестве этого начального значения первый элемент списка. Соответственно, форма без identity кидает исключение для пустой коллекции.

Всегда ли такое поведение всем нужно? Почему не вернуть какое-нибудь Optional и дать пользователю самому решить, что делать в случае пустой коллекции? Да или хоть null вернуть, раз уж это null-friendly язык.

●  Давайте ещё навалим про библиотеку. Нафига в стандартную библиотеку языка, который поддерживает дата-классы, включили пары? Это ж прямое поощрение плохого кода.

Напоминаю, дата-классы выглядят так:

data class User(val name: String, val age: Int)
val duncan = User("Duncan MacLeod", 426) 
val (name, age) = duncan
println("$name, $age years of age") // печатает "JaDuncan MacLeodne, 426 years of age"

Пара выглядит вот так:

val (name, age) = Pair("Java", 23)
println("$name, $age years of age") // тоже печатает "Java, 23 years of age"

А все потому, что внутри:

public data class Pair(
    public val first: A,
    public val second: B
)

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

●  Очень странный момент — возможность не указывать возвращаемый тип метода (особенно публичного).

Совсем недавно был случай в C++, от которого меня чуть не разорвало от злости. Программа падала в произвольном месте, а я не понимал — почему. Оказалось, в C++ можно не писать return в методе, который согласно сигнатуре должен что-то возвращать. Это не синтаксическая ошибка согласно стандарту, а undefined behavior. Соответсвенно, программа в рантайме падает с произвольной ошибкой. Чудесный язык — в нем есть специальный синтаксис для неработающих методов. С тех пор я очень аккуратно проверяю, что мы обещали вернуть из метода и что отдали на выходе. Эдакая параноидальная привычка.

И вот теперь, в лучшем в мире языке Kotlin мы можем вообще не указывать возвращаемый тип. Это провоцирует людей писать нечленораздельную лапшу, в которой и ничего не понятно. Если метод a вызывает метод b, а тот метод c, а тот содержит в теле выражение when, в котором в ветках ещё три метода вызываются d, e и f, попробуй пойми тип метода а!

fun a(check: Int) = b(check)
fun b(check: Int) = c(check)

fun c(check: Int) =
    when (check) {
        1 -> d()
        2 -> e()
        else -> f()
    }

fun d() = "result 1";
fun e() = "result 2";
fun f() = "result 3";

fun main(args: Array) {
    println(::a.returnType)
    for (i in 1..3)  println(a(i).javaClass.name)

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

Изначально в нашем примере выхлоп выглядел вот так:

kotlin.String
java.lang.String
java.lang.String
java.lang.String

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

fun d() = "1";
fun e() = 100500;
fun f() = listOf();

И результат тут же изменится на

kotlin.Any
java.lang.String
java.lang.Integer
kotlin.collections.EmptyList

Никакой кристаллизации API. Для публичных методов явная спецификация API должна быть священной коровой, а Kotlin её не требует.

Пожалуй, для начала достаточно. Из этой статьи может показаться, что все в Котлине плохо, но это очевидно, не так. Как минимум, зарплата Kotlin-разработчика обычно неплоха :-)

В недавнем докладе на Joker 2018 (есть слайды), Паша (asm0dey) Финкельштейн отмечал, что на бэкенде Kotlin помогает писать более красивый и лаконичный код (но не всегда это получается), на нем получаются более выразительные тесты, с ним работает GraalVM, и всё это с примерами для Spring, Spring Security, Spring Transactions, jOOQ, и т.п.

Стоит ли переходить на Kotlin с Java для мобильных приложений? Неясно. В любом случае, Kotlin интересный. Давайте в нём покопаемся!


Минутка рекламы. Уже на этой неделе, 8–9 декабря 2018, пройдет конференция Mobius. На ней, скорей всего, можно будет пересечься со множеством людей, реально использующих Kotlin, и узнать, зачем и как они это делают. Места все еще есть, а вот времени уже почти не осталось, так что, если хотите прийти, сейчас у вас последний шанс. Билеты можно приобрести на официальном сайте.

© Habrahabr.ru