«Мы даже не пытаемся запустить старый код, такой задачи у нас не стоит в принципе» — Роман Елизаров о разработке Kotlin

Если хочешь в чем-то разобраться — учись сразу у лучших. Сегодня на мои вопросы отвечает бог корутин и concurrency, Рома elizarov Елизаров. Мы поговорили не только о Kotlin, как вы могли бы подумать, но ещё и о куче смежных тем:

  • Golang и горутины;
  • JavaScript и его применимость для серьезных проектов;
  • Java и Project Loom;
  • олимпиадное программирование на Kotlin;
  • как правильно обучаться программированию;
  • и другие волнующие вещи.


xhux9llonplkxeenzbn6dic-z6a.jpeg

Привет. Давай вначале пару слов о себе. Ты давно занимаешься Kotlin?

У меня с Kotlin давняя история. В 2010 году Kotlin начинался как проект в JetBrains, где я в тот момент еще не работал. Но Макс Шафиров (он тогда занимался Kotlin и был одним из инициаторов этого движения внутри JetBrains) пригласил меня стать внешним экспертом и посмотреть на дизайн, прокомментировать. Изначально язык дизайнился для решения своих проблем, ведь у JetBrains своя большая база кода на Java, с понятными проблемами, которые в коде постоянно есть, и хотелось сделать язык для себя, чтобы свой код писать приятней, эффективней, с меньшим количеством ошибок. Просто провести у себя модернизацию. Естественно, это быстро переросло в идею, что раз у нас такие проблемы — значит, и у других такие проблемы есть, и им нужно было подтверждение от других людей, что они идут правильным путем.

Меня пригласили как эксперта, чтобы я посмотрел и сверил то, что происходит, с тем, что надо. Про nullability — это я настоял, что этим надо заниматься, потому что мне в тот момент было очевидно, что если ты пишешь на Java, там много проблем, но nullability — это основная беда, на которую постоянно наталкиваешься.

В самой работе команды я не участвовал, просто периодически поглядывал, участвовал в соревнованиях на Kotlin (Kotlin Cup). Я всю жизнь занимаюсь соревнованиями, но сам уже тогда активно не участвовал. Например, я бы не вышел в финал соревнований вроде Facebook Hacker Cup, форма не та из-за того, что в соревнованиях уже не участвую на постоянной основе. А в Kotlin Cup я принял участие и, так как он не собрал широкую аудиторию, я легко вышел в финал.

На тот момент (2012–2013 гг.) Kotlin представлял собой грустное зрелище с точки зрения тулинга, потому что там всё тормозило. С тех пор команда проделала огромную работу. Я пришел в команду два года назад, сразу после релиза 1.0 и до того, как Google официально признал язык. В команде я занялся всякой асинхронностью и корутинами, просто потому что так вышло, что у меня подходящий опыт, я много в DevExperts занимался всякими разными большими энтерпрайзными системами, и там много асинхронности и коммуникации. Поэтому я хорошо представлял себе проблемные места — что надо чинить и что у людей болит. Это очень хорошо легло на нужды Kotlin, потому что болит не только у нас. Болит у всех. Даже в JVM занялись Project Loom, что как бы намекает, что болит у всех. Я до сих пор занимаюсь котлиновскими библиотеками, и основной наш фокус — на всякие connected-приложения и асинхронность.

То есть ты занимаешься в основном библиотеками, не компилятором и вот этим всем?

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

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

Да, можно найти кучу всяких задач, потому что я постоянно на что-то наталкиваюсь.

Ты упомянул Project Loom. Его сделал парень, который сделал Quasar. Cо стороны это выглядит очень забавно, я как раз хотел на Хабру писать статью про Loom. Можешь рассказать что-нибудь про него?

Видел презентацию, идея понятная. Корутины и асинхронное программирование нужны всем. Например, на прошлом JPoint ребята из Alibaba рассказывали, как они хакнули JVM и прикрутили себе файберы хотспот, просто накатив туда патчик, который даже не они написали, а какие-то ребята до них. Они уже потом подпилили под себя. Замечательный доклад. Очень рекомендую.

А ты рекомендуешь так делать?

Так делать в энтерпрайзах приходится. Каждый большой энтерпрайз, выше какого-то размера, когда у тебя начинает работать несколько тысяч человек (а для кого-то и меньше), мейнтейнит свой хак OpenJDK. И конечно, если у тебя есть бизнес-критичные юзкейсы, то почему бы и не [акнуть что-то под себя, не вижу в этом никакой большой проблемы. Не то чтобы я это рекомендую, но приходится. Если в HotSpot нет легковесных потоков, то что делать? Это, собственно, говорит о том, что людям надо, что назрело. И фидбэк, который мы получаем по корутинам, тоже говорит о том, что да, назрело, людям нужны легковесные потоки, у людей вагон юзкейсов для легковесных потоков. Тот факт, что они должны как-то поддерживаться в JDK, давно назрел, и в этом смысле я не сомневаюсь, что когда Loom рано или поздно дойдет по продакшна, это будет востребовано. Есть люди, которым это надо. Есть люди, которые даже ради этого патчат HotSpot.

Видел, частая проблема — у тебя есть какой-то веб-сервер, в него много людей стучится, и он начинает блокироваться на тредах.

Это довольно типичная проблема. И веб-сервер, и application-сервер, и бэкенд. Если ты посмотришь ту же презентацию Алибабы, почему и понадобилось это дело, то у них не веб-сервер, у них классическая энтерпрайзная архитектура, у них на бэкенде на Java написаны всякие сервисы, эти сервисы находятся под нагрузкой. Я с таким же работал в DevExperts: сервисы под нагрузкой, тебе приходят запросы, которые ты не сам ведь обрабатываешь — в современном мире у тебя всё connected. И вот этот запрос ты не сам обрабатываешь, а еще 100500 всяких других сервисов вызываешь и ждешь, пока они ответят. И если эти сервисы тормозят, то у тебя много потоков ждет. Ты не можешь себе позволить иметь десятки тысяч этих ждущих потоков. И у тебя получается просто из-за какой-то ерунды следующее: один сервис, который ты используешь, тормозит, и куча потоков стоит и ждет. И сейчас это очень большая проблема.

Одна из причин, почему люди массово мигрируют на Go — не потому, что язык хороший, а потому что там легковесные потоки из коробки, и такой проблемы уже нет: горутины могут ждать, и они ничего не стоят. В том же Alibaba, решение, которое они заимплементили — оно вообще тупое из всех тупых. Они не очень легковесные в том смысле, что они каждой корутине выделяют один большой стек по 2 мегабайта, хакнув HotSpot, чтобы можно было эти стеки переключать. Они экономят физический поток, но не экономят стеки. И для них решение работает — оно, кстати, очень простое, у них патч HotSpot, насколько я понимаю, не очень большой. Ребята из Loom затеяли нечто более глобальное. Они решили сэкономить не только на физических потоках, но и на стеке, чтобы не тратить 2 мегабайта на поток. В прототипе текущий стек через HotSpot проходит, его копируют в маленькую хиповую структуру. И могут дальше этот физический стек переиспользовать для других целей.

Но там есть такой хитрый хак: когда ты возвращаешься назад на исполнение, они копируют его не весь, а только самый верх.

Да, там вагоны хаков и оптимизаций. Что в итоге из этого получится — очень сложно сказать. Потому что на примере подхода с копированием, сразу возникает следующая проблема:, а что делать с нативными вызовами? Изнутри нативного вызова ты уже не можешь скопировать стек нативного вызова. В подходе Alibaba такой проблемы нет. Нативный, не нативный — какая разница, ты просто тот стек отцепил совсем и оставил его в покое, подцепил другой стек, всё работает. И тут рано говорить, что получится или не получится, с этим нативным стеком иногда можно жить, иногда нельзя — на этом этапе рано сказать. Например, как это в Go реализовано — там совсем другой механизм. Пока ты выполняешь гошный код, используются маленькие гошные стеки. Соответственно, когда гошный рантайм вызывает функцию, он смотрит, сколько нужно стека. Если текущего стека не хватает, он перевыделяет — увеличивает размер выделенного стека. Если, соответственно, ты делаешь нативный вызов, то они уже берут какой-то большой нативный стек из некоего пула и используют его.

И для гошного кода тоже?

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

Нам постоянно задают вопрос: «Что быстрее? Что подходит? Как вы в корутинах это делаете?» Мы в корутинах не хакаем JVM. Наша задача заключается в том, чтобы это работало под обычным JVM. И чтобы на Android тоже работало. Там свой ART, который тоже о корутинах ничего не знает. И поэтому, естественно, нам приходится ручками генерировать байткод, который делает что-то очень похожее на копирование стека, который делает Loom, только мы это делаем в байткоде. Берем его, когда он уже засаспендится. Берем стек, разматываем и копируем в хип. Мы не на рантайме, который бы это за нас делал, у нас сгенерирован байткод, который это делает. Он сохраняет и восстанавливает состояние корутины. Из-за того, что мы не делаем рантайм, естественно, у нас от этого больше оверхеда. В рантайме ты можешь все сделать быстрее. С другой стороны, если ты корутины используешь для асинхронного программирования, то тебе надо заснуть, если ты ушел ожидать ответа от какого-то сервиса, а послать запрос в какой-то сервис так дорого, что весь оверхед на копировании стека вообще никого не волнует — медленный он у тебя или быстрый — вообще становится неважно. Да, если ты это используешь именно для асинхронного программирования. У нас на корутинах в Котлине это замечательно скейлится, как и показано в прототипе Project Loom.

Другое отличие — так как мы в Котлине вынуждены делать это в байткоде, то у нас есть такой интересный побочный эффект. С одной стороны, вроде бы и неудачно, а с другой — наоборот. Заключается он в следующем: нельзя усыпить произвольную функцию. Нужно функции, которые могут заснуть, помечать модификатором suspend — явно пометить, что функция может приостановиться и чего-то ждать, что она долгая. С одной стороны, в Loom тебе это не нужно, потому что рантайм может усыпить что угодно. В решении от Alibaba то же самое — ты можешь у любого потока отобрать стек. Или в Go — там всё можно засаспендить, любой код может уснуть. Наплоди еще горутин и делай. С одной стороны, этот подход очень похож на программирование с тредами. Ты как бы программируешь как раньше, только теперь треды называются файберами и стали очень дешевыми. Если внимательно посмотреть презентацию того же Loom, выясняется, что файберы и треды — это всё-таки разные вещи. Как сделать так, чтобы старый код, который написан с тредами, прям совсем из коробки завелся на файберах — не очевидно, и что у них получится — никто не знает. Там начинаются проблемы:, а что делать с дэдлоками, что делать с кодом, который соптимизирован на thread locals, опять же какие-то хэши свои локальные имеет или по thread ID хитро какие-то перформанс-оптимизации делает. И в Go та же самая проблема — когда хардварные thread ID не экспозятся, писать какой-то high performance-алгоритм становится нетривиально.

А в Котлине такого нет?

В Котлине мы же не пытаемся сделать вид, что тред и файбер — это одно и то же. Мы даже не пытаемся запустить старый код, такой задачи у нас не стоит в принципе. Мы говорим: «Извините, так как мы не рантайм, мы не можем произвольно взять старый джавовый код и начать там что-то переключать». И даже не будем пытаться. У нас другая задача. Мы говорим, что у нас есть фича языка, засыпающии функции, вы можете с ними писать асинхронный код, и это новая фича языка. И от этой проблемы («как запустить старый код») мы таким образом полностью дистанцируемся, мы говорим: «Вот есть новый код, хороший, православный, его можно усыплять». В какой-то степени это делает жизнь проще, потому что не надо парить голову ни себе, ни людям, а что происходит, если какой-то старый говнокод, который не знал, что его будут на файберах запускать, вдруг на них запустят.

У нас в нашей модели нет никакого старого кода, только новый, который изначально готов к тому, что сегодня он на одном треде, завтра на другом, и если ему, например, нужно узнать, какой сейчас тред, он это узнает. Да, нужен thread local, но он может их узнать. Однако он должен быть готов к тому, что сегодня thread locals одни, а завтра — другие. Если он хочет, чтобы эти локалы путешествовали с ним, для этого есть другой механизм, корутинный контекст, где он может хранить свои вещи, которые будут вместе с корутиной путешествовать с треда на тред. Это, в каком-то смысле, нам упрощает жизнь, потому что мы не пытаемся старый код поддерживать.

А с другой стороны, мы заставляем человека явно подумать над своим API, сказать: вот я пишу функцию на Kotlin с корутинами. Если раньше я смотрю на какой-то метод в своем коде, getЧтоНибудь, непонятно, этот метод быстро работает и возвращается сразу или пойдет в сеть и может час работать — я могу только документацию почитать и понять, как быстро он будет работать. А может, сейчас он быстро работает, а завтра придет программист Вася Пупкин и сделает так, что он теперь ходит в сеть. С Kotlin-корутинами мы даем гарантированный языком механизм с модификатором suspend. Я когда сам работаю с корутинами, смотрю на какую-то функцию, если не вижу модификатора suspend, значит, она быстро работает, всё локально делает. Есть модификатор suspend, значит, эта функция какая-то асинхронная, она пойдет надолго в сеть. И это помогает делать самодокументирующийся API, чтобы мы сразу видели, что нас ожидает. Это помогает сразу избежать тупых ошибок, когда я где-то забылся и где-то в коде вызвал что-то долгое, не подозревая об этом.

На практике это оказывается очень хорошо. Вот эта необходимость явно разметить эти засыпающие функции. В Go, например, этого нет, я не обязан там ничего размечать. Выясняется, что этот побочный эффект нашей имплементации (что надо размечать модификатором suspend) помогает тебе сделать правильно архитектуру, помогает тебе контролировать, что ты не вызовешь какую-то случайную дико долгую асинхронную дичь в месте, где ты изначально ожидал, что всё произойдет быстро.

Но есть же часть вещей, которые сложно запретить, например, какой-нибудь сетевой IO, файловый.

Нет, сетевой IO как раз запретить достаточно легко. Вот файловый IO — сложно. Но здесь опять тонкий момент: для большинства приложений файловый IO — это быстрая вещь, и поэтому совершенно нормально, что он работает синхронно. Очень редкое приложение так много работает с IO, что для него становится проблемой тот факт, что это занимает так много времени. И здесь мы даем человеку возможность выбрать: ты можешь у нас напрямую делать файл IO и не париться, потому что оно будет блокировать, что происходит (потому что обычно это быстро). Но если конкретно в твоем кейсе просто какое-то очень долгое вычисление, вроде не асинхронное, но просто жрет кучу времени CPU, и ты не хочешь этим самым блокировать какие-то свои другие thread-пулы, мы предоставляем простой понятный механизм: ты заводишь отдельный thread pool для своих тяжелых вычислений, и вместо того, чтобы писать обычную функцию, которая fun computeSomething (), и писать в документации «Чуваки, аккуратно, эта функция может работать очень долго, поэтому внимание — не используйте ее где попало, не используйте в UI», мы предлагаем более простой механизм. Ты просто пишешь эту функцию как suspend fun computeSomething (), а для её реализации используешь специальную библиотечную функцию withContext, которая перекидывает вычисление на указанный тобой специальный thread pool. Это очень удобно: пользователю не надо больше парить мозг: он сразу видит suspend, знает, что этот вызов его тред не блокирует, и он может совершенно спокойно вызывать её из UI-потока и так далее.

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

Я думаю, насколько это безопасно. Может ли кто-то сломать thread pool или вломиться в чужие данные?

Естественно, всё возможно. От кривых рук тяжело защитить. Понятно, что сколько бы мы ни писали в компиляторе всякие системы типов и проверки, всегда всё можно сломать. Вопрос в том, что компилятор должен помогать писать правильный код. К сожалению, мечта запретить писать плохой код утопична. Мы специально не включаем какие-то фичи в язык. В Котлине нет каких-то вещей из Java, если про них известно, что они в основном используются не по назначению и код с ними в основном плохой. Но и любую хорошую фичу, которая есть в Котлине, можно использовать не по назначению массой разных способов. Вариантов нет, к сожалению. Язык можно абьюзить по-разному. От кривых рук не защитишься никак.

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

От каких вопросов?

Например, один вопрос пережил двух человек: почему в Котлине нет raw types? Без дженериков.

Потому что всегда можно написать звёздочку.

А оно при этом будет совместимо с Java?

Да.

То есть у тебя есть какой-то джавовый метод, который требует что-то без дженерика. List, например.

Если есть такой джавовый метод без дженерика, ты туда можешь передать любой List. Можешь List со звёздочкой передать, можешь со строкой. Котлин позволит тебе в джавовый метод передать любую дичь. Если он возвращает тебе raw List, то в Котлине ты получишь некий platform type, который ты можешь закастить, например, к List со звездочкой. Потому что raw type, по сути, в Java сделан не от хорошей жизни, а чтобы было проще мигрировать. Была Java, в которой не было генериков, теперь есть генерики, и чтобы люди не мучались, чтобы им не нужно было по всему коду эти угловые скобочки проставлять, была сделана эта специальная фича, raw type. Когда делали Котлин, такой проблемы не было — не было никакого старого кода, в котором генерики не указаны. Есть только новый код, в котором все типы — генерики. Поэтому проблема миграции со старого кода без генериков на новый код с генериками — её не существует, и нет смысла делать в языке такую фичу, как raw types. Она Котлину не нужна, так как тут нет migration story.

Как работает кастинг platform type к котлиновскому?

Платформенные в Котлине считаются flexible. Это проще рассмотреть на примере nullable-типов. Вот у тебя есть джавовский код, который возвращает String. В Котлине непонятно, там String или nullable String — это два разных типа. И мы ведь не знаем, какой String возвращается из джавовского метода — nullable или не-nullable. Поэтому Kotlin считает, что он может быть и такой, и сякой. И разрешает тебе его присвоить как в String, так и в nullable String. То же самое и здесь, когда ты получаешь какую-то слаботипизированную дичь от Java, ты на стороне Котлина всегда можешь указать просто более специфичный и правильный тип.

А если ты указал неправильно?

Если ты указал совсем неподходящий, то у тебя компилятор, естественно, ругнется, потому что он должен подходить. Flexible не значит что угодно, это не dynamic. Flexible указывает на диапазон — то, что сейчас вернула Java, ты потом можешь присваивать и в String, и в nullable String, но не в int.

Там всё-таки есть какое-то прямое перечисление возможного, и оно там матчится.

То же самое с сырыми типами, похожий механизм, но немного посложнее. То же самое с коллекциями. В Котлине коллекции разделяются на read only и mutable. У тебя бывает List и MutableList. Если тебе Java возвращает List, то фиг поймешь, что Java-программист имел в виду, можно мутировать его или нельзя, поэтому ты уже на Котлин-стороне можешь в List присвоить или в MutableList. Котлин более строго типизирован, чем Java. Соответственно, когда ты из Java что-то получаешь, ты должен более специфичный тип указывать и наоборот. В обратную сторону работает из коробки, так как джавовский метод принимает менее типизированные вещи, туда можешь любую подходящую котлиновскую штуку передать. Опять же, принимает, например, джавовский метод List, мы же не знаем, какой, поэтому можно туда и MutableList, и обычный List передать. А если ты будешь передавать не List, а String, то не разрешит.

А когда используются джавовские либы какие-то либо, какой правильный подход? Надо заворачивать в какие-то врапперы котлиновские?

Не, ничего не надо заворачивать. Котлин задизайнен так, чтобы джавовские библиотеки было просто использовать. It just works. Большинство Java библиотек просто работают с Котлином вообще без проблем. А если там еще nullability-аннотации прописаны, то Котлин сразу видит, nullable или нет результат. Но удобство зависит от дизайна библиотеки, конечно. У Котлина, например, специальный синтаксис, когда последнюю лямбду удобно передавать за круглыми скобочками. Например, в джавовых либах, у которых такой же порядок аргументов, понятно, что последним аргументом принимается какой-нибудь предикат. В Котлине ты можешь использовать его без всяких специальных адаптеров, просто можешь использовать красиво по-котлиновски. Берешь какую-нибудь JavaFx, и без каких-либо дополнительных адаптеров код на Котлине получается красивее, чем если бы ты JavaFx использовал на Java, удобнее, приятнее его смотреть. Понятно, что ты можешь еще написать себе каких-то адаптеров, и это будет еще круче. Но даже без них джавовские либы приятно использовать. Любая либа становится круче и удобнее, если ты просто начинаешь использовать её из Котлина, просто по факту использования Котлина.

У тебя аж голос изменился, так эмоционально всё описываешь.

Конечно. Я просто видел это много раз, тебе просто так приятнее программировать. Мы на такое же надеемся и с Kotlin Native. Идея та же: Котлин приятнее как язык, и мы там пытаемся сделать максимально seamless интероп со всякий C-шной экосистемой, просто чтобы дать людям возможность использовать их существующие либы из более приятного языка, без каких-либо барьеров.

Кстати, а ведь при переходе на Native там не должен ли измениться смысл языковых конструкций?

Конечно, у нас, даже если посмотришь на Kotlin JavaScript, какие-то конструкции немного по-другому работают. Мы не ставим целью добиться «Write once, run anywhere», не стоит такой цели, чтобы было абсолютно идентично. Наша задача немного другая: мы хотим некое подмножество языка, вроде Common Kotlin или Portable Kotlin, на котором ты будешь писать, и оно будет работать отовсюду. Понятно, что ты можешь залезть в какие-то платформо-специфичные штуки, и их поведение будет соответствующим, но это нормально. Если ты пишешь под одну платформу, и тебе всё равно, а под несколько платформ ты просто какие-то вещи будешь обходить стороной. И мы многие вещи специально не фиксим, чтоб сохранить перформанс.

На JVM у нас перформанс, как у Java, на JS у нас перформанс почти такой же, как у JS, на Native нативный перформанс. Задача, в первую очередь, не смотря на эту переносимость, дать нативной платформе высокопроизводительный код, а не писать еще одну виртуальную машину. Многие пытаются транспилировать какую-нибудь джаву в тот же JS, попытка полностью эмулировать джаву. Эта попытка приводит к огромному перформанс-оверхеду. Какие-нибудь банальные глупые вещи: ты берешь double, конвертируешь в String. И на JVM-ном Котлине число 0 превратится в строку »0.0», а на нативном JS будет просто ноль. Разное поведение. Можно было бы на JS это пофиксить, но тогда все преобразования чисел в строки стали бы намного тормознее, потому что он будет обвешан дополнительными проверками — кому это надо? Пускай будет эта небольшая разница, зато нативный перформанс. У нас нет никакой своей специальной тормозной функции, которая преобразовывает числа в строки, у нас просто нативное JS-ное преобразование. Таких примеров очень много, где мы специально принимаем решение сделать разное поведение из перформанс-соображений. Но там, где это не критично. Всё-таки семантика основных конструкций языка — классы, наследование, вызовы и так далее — все работает так же. У нас есть пример огромных проектов, как собственных, так и внешних, в которых написано много кода на Котлине, он компилируется и работает под JVM, JS, Native — и всё это нормально работает, даже несмотря на то, что где-то поведение немного отличается. Все тесты проходит.

А корутины работают на всех платформах?

Да. Это фича языка, которой всё равно, под какую платформу ты её запускаешь.

То есть вся размотка стека, вот это всё…

Да, это всё исключительно компиляторная фича. Нам ведь не нужна поддержка от платформы, вот в чем фишка. В отличие от проекта Loom и так далее. Мы не делаем это каким-то хаком в JVM. Это фича компилятора, поэтому мы можем то же самое сделать под любую платформу.

А если взять котлиновский код и попытаться его зареверсить, например, в Java?

То ты увидишь там всю эту дичь, которую компилятор выдал. Зато у тебя красивый код на входе. В смысле — зато ты написал красивый код. Это уже задача компилятора сделать из этого нечто, что будет работать. В Котлине много конструкций высокого уровня, которые потом превращаются в какую-то низкоуровневую пургу. Например, пишешь for (i in 0…10), и это разворачивается в цикл for (int i = first и прочую дичь. Но писать приятно, так что какая разница, как оно там компилируется. Оно работает. Быстро, потому что разворачиваются в соответствующие нативные конструкции.

А кто-нибудь сравнивал одни и те же программы на разных платформах?

Нет, и более того… несмотря на цель сохранить перформанс, платформы изначально несравнимы. У них разные задачи. Какой смысл сравнивать JVM и JS.

А какой смысл писать на сервере на Node.js?

Так понятно зачем! Не ради перформанса. Люди пишут для того, чтобы реюзать своих JS-программистов. Зачем изучать новый язык, новых программистов нанимать, если JS-программисты спокойно пишут бэкенд. Мы такую историю хотим дать с более хорошим, типизированным языком. Если ты умеешь программировать на Котлине, то один раз написал бизнес-логику и дальше пожалуйста — гонять ее на джавовом бэкенде, годняй на JS-фронтенде, гоняй в нативном микросервисе или чем-то — пофиг. Если код на Котлине, он сможет скомпилироваться куда угодно. В этом как раз цель Котлина.

У тебя были доклады вроде «миллиона котировок». Ты в своих предыдущих проектах стал бы использовать Котлин, если бы он был изобретен сильно раньше?

Конечно. Всё, что я говорил в те времен, а можно делать на Котлине. На JVM это просто более удобный язык, чем Java, компилирующийся в тот же самый байткод. Поэтому не использовать его на JVM большого смысла нет. Разве что у тебя legacy, enterprise, и это просто запрещено. Или если ты делаешь библиотеку, которую должны использовать клиенты, которые Котлин не могут использовать. А если ты пишешь для себя, нет никакого смысла не писать на котлине под JVM. Байткод тот же самый, а на входе — не только более удобный, но и более компактный язык. Он еще и более типизированный, защищает от большего количества ошибок. Но не заставляет писать совсем жестко типизированную дичь, — как всегда в JVM можно сказать компилятору «я знаю лучше тебя». Механизмы обойти компилятор у тебя есть. Но в обычной практике API на Котлине получаются более документированными, более строгими и более безопасными. Код получается надежней, реже падает по исключениям. Его читать проще, меньше воды в коде. Многие штуки, когда в Джаве пришлось бы писать бойлерплейты, в Котлине пишется в одну или несколько строчек. С таким кодом приятней работать, в нем меньше воды и больше сути, которую ты хотел выразить.


В эти выходные Роман будет на фестивале TechTrain с докладом «Зачем нужен еще один язык программирования?». Об этом фестивале мы совсем недавно писали на Хабре. Загляните, вдруг понравится.

Раз уж ты начал говорить про JS и обучение, насколько сложно на Kotlin переучиться с Java?

Вот с Java как раз очень легко и вообще никаких проблем. У нас есть и книжка «Kotlin in Action», и сайт, ориентированный на Java-программистов. По опыту, Java-программисту нужно потратить от двух дней до двух недель, и всё, вышел из тебя отличный Kotlin-программист. И это не случайно получилось, это «by design». Изначально в дизайне Котлина заложено, что Java-программистам должно быть легко на него перейти. Велосипед не изобретали. У Андрея Бреслава на прошлом JPoint есть хороший доклад, откуда что Kotlin позаимствовал. Есть слайд о том, откуда родились разные языковые конструкции. Видно, что 60–70% взялись из Java. Оно и называется как в Java, чтобы было проще, чтобы не нужно было изучать что-то сильно новое. Это большие языки типа Java могут себе так позволить.

У них вообще есть такая дизайн-цель — об этом еще Гослинг говорил, что если мы что берем, то обязательно называем по-другому. Никогда ничего не берется as-is. Мы люди большие, можем себе позволить, пускай люди учат. В Kotlin же, если ты уже знаешь, что такое «класс», это и должно называться «классом». Если знаешь, что такое «интерфейс» — должно называться интерфейсом. Люди знают цикл while — нет смысла его переименовывать. Хотя можно найти 100500 более хороших названий для него. Но зачем? Кроме того, большинство наших обучающих материалов рассчитано на Java-программистов. Даже я делал какие-то доклады, «Введение в Kotlin», и все это расчитано на них, нужно рассказать только какие основные вещи в Котлине новые и интересные.

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

Для таких новичков, что совсем-совсем, или для перебежчиков с других языков?

Да, для совсем новичков. Еще стараемся сотрудничать с вузами. На пути переучивания Java-программистов мы очень далеко продвинулись. Android-программисты тоже — больше половины, вроде бы по последним данным. И все переучились без проблем. Миллион программистов как минимум уже переучились, и никаких проблем нет. А вот на обучении с нуля — мы находимся на очень раннем этапе пути.

А есть какие-то шутки, которые людям сложно понимать с нуля?

Они такие же, как в других языках. Людям сложно понимать вложенные циклы, рекурсию, референсы. Это известная тема, неважно, на каком языке ты программируешь. Обучение человека с нуля — это некое искусство, там есть сложные моменты, которые надо адекватно объяснять. Но в этом плане Kotlin очень хорош. Например, мы получаем фидбэк от университетов. Если взять базовый курс программирования в каком-нибудь ВУЗе и посмотреть, чему их учат, окажется, что никто не учит сразу классам. Обычно учат простым процедурным вещам: как писать циклы, как писать функции вызывать и так далее. Раньше многие учили на C++, потом ринулись в Java, теперь на Python. Но не каждый язык одинаково хорош. Та же Java была когда-то очень популярна, и до сих пор многие вводные курсы читают на Java, но это не самый лучший язык именно для вводного курса программирования. Чтобы написать простой хэлловорлд, нужно написать класс, puiblic static void main… А ты же учишь не объектно-ориентированному, а процедурному программированию. В Java, чтобы что угодно написать, надо объявлять класс. Зачем новичку парить мозг?

Kotlin в этом плане больше подходит для обучения: открываешь файл, пишешь функции свои, почти как в Python, только с типами. По сравнению с C++ можно поспорить, потому что C++ очень большой язык. Это не значит, что на плюсах нельзя учить программистов, можно. Но когда учат на плюсах, то учат очень ограниченному подмножеству. Рассказывают небольшие фишки, очень аккуратно, чтобы обучающийся не сделал шаг влево и шаг вправо. А Kotlin — хороший типизированный язык. Если хочешь хорошему нетипизированному языку научить — это Python. Я свою дочку в качестве первого языка программирования научил Питону. Чтобы не забивать ей голову типами сразу. Когда ты вообще не умеешь программировать, тебе нужно очень много узнать. Не бывает так, чтобы ты сразу всё узнал. Нужно постепенно учить. Поэтому проще вначале научить всяким императивным конструкциям — циклы, ввод, вывод, функции, процедуры —, а типы отложить пока в сторонку. Следующий же язык должен быть типизированным, чтобы разобраться с типами.

То есть ты считаешь, что типизация — полез

© Habrahabr.ru