Pull to refresh
136.57
JUG Ru Group
Конференции для Senior-разработчиков

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

Reading time6 min
Views55K

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


  • Бонус: хаброопрос «Как вы используете Kotlin?»



●  В 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<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>

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


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


fun List<String>.filterValid(): List<String>

@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>

А как вам такое: в Kotlin нет checked exceptions. А в Java-реальности они есть. Отряд специального назначения «Боевые протезы» имеет честь представить новый самоходный костыль @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<Int>.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<out A, out B>(
    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<String>) {
    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<String>();

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


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. На ней Святослав Щербина из JetBrains расскажет о том, как писать мобильные приложения на Kotlin Muplitplatform. Кроме того, можно будет пересечься со множеством людей, реально использующих Kotlin, и узнать, зачем и как они это делают. Места все еще есть, а вот времени уже почти не осталось, так что, если хотите прийти, сейчас у вас последний шанс. Билеты можно приобрести на официальном сайте.
Only registered users can participate in poll. Log in, please.
Как вы используете Kotlin?
24.66% Пишу на Kotlin для мобилок272
12.69% Пишу на Kotlin для бэкенда140
1.99% Пишу на Kotlin для фронтенда22
3.99% Пишу на Kotlin для чего-то кроме бэк/фронт/мобайл44
19.76% Не пишу на Kotlin, но думаю начать218
30.01% Не пишу на Kotlin, зашел поржать и обсудить в комментариях331
28.38% Не пишу на Kotlin и не собираюсь313
1103 users voted. 177 users abstained.
Tags:
Hubs:
Total votes 143: ↑91 and ↓52+39
Comments319

Articles

Information

Website
jugru.org
Registered
Founded
Employees
51–100 employees
Location
Россия
Representative
Алексей Федоров