Как стать автором
Обновить

Комментарии 48

Понятно, что в официальном блоге JetBrains такого прямого противопоставления не будет, но от коммьюнити хотелось бы четкого списка пунктов «Почему Kotlin, а не Scala».

Насколько я понимаю, в Scala недостаточно хорошо продумана обратная совместимость — как между версиями самой Scala, так и интероперабельность из Java в Scala, и наоборот. Например, те же properties в Scala не используют геттеры и сеттеры по умолчанию, а для совместимости нужно использовать какие-то дополнительные аннотации. Как на уровне байткода будут выглядеть геттеры и сеттеры в Kotlin?

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

Как со всем этим делом в Kotlin? Правильно ли я понимаю, что стараются устранить недостатки Scala, и это и есть основная киллер-фича?
UPD. На официальном сайте есть краткое сравнение: https://kotlinlang.org/docs/reference/comparison-to-scala.html
Основная киллер-фича котлина, что они не пытаются делать киллер-фич, а нацелены на разработку простого и удобного инструмента для реальной разработки. Конечно, можно пытаться сравнивать скалу и котлин, и в каждом посте обязательно про это бывает, но лично я не вижу большого смысла про это много писать — в статье по этому поводу раздел "Простой и совместимый". Это же и главные отличия от скалы, на мой взгляд.
Как на уровне байткода будут выглядеть геттеры и сеттеры в Kotlin?

Можно написать пару маленьких программок и декомпилировать.
геттеры и сеттеры будут реализованы как методы getVarName() и setVarName(...)

Но мне синтаксис Scala кажется более простым. В kotlin больше сущностей и ключевых слов.

Например, для переопределения += в scala достаточно создать метод с очевидным названием "+=", причём нет никаких ограничений на тип и количество аргументов — это самый обычный метод, а не какая-то дополнительная сущность.

В kotlin придётся написать в стиле operator fun plusAssign , что не совсем интуитивно (приходится вспоминать, что и как называется), и есть ограничения — метод не может возвращать значение.
Причём в kotlin можно переопределить только небольшой список операторов, а в scala свободы побольше (например, можно определить := или ++=).

На самом деле, scala-метод типа += будет создан с именем "$plus$eq", которое не очень удобно вызывать из java. Было бы идеально, если бы в kotlin стало можно давать методам имена типа "+", которые на самом деле отображались бы в "plus" и.т.п.

Или, например, синтаксис объявления метода:
В scala: def methodName(...) = {...}
В kotlin возможны два варианта — как в scala (со знаком =) и как в java (без него), но эти два способа объявления неэквивалентны друг другу и работают немного по-разному, я однажды кучу времени потратил на поиск такой "особенности" в коде.

Null-safety: интересная штука, но на практике во все публичные методы добавляются проверки для каждого аргумента на null. Мне кажется несколько избыточным — и есть подозрение, что это негативно влияет на производительность. (Там не просто проверка, а вызов статического метода с передачей объекта-параметра и его имени как строки, чтобы можно было кинуть понятное исключение)

Что понравилось в kotlin:
1) inline. Никаких созданий объектов, просто подстановка тела функции.
2) extensions. Тоже удобная штука — и никаких потерь производительности.
В Scala похожая функциональность сделана через implicit-преобразования, из-за чего при вызове создаётся ещё один объект.
Или, например, синтаксис объявления метода:
В scala: def methodName(...) = {...}
В скале на самом деле тоже есть синтаксис без "=", но он deprecated.
В Scala похожая функциональность сделана через implicit-преобразования, из-за чего при вызове создаётся ещё один объект.
В scala есть extends AnyVal. Для таких классов тоже не создается экземпляров, если это возможно.
В Linker собираются автоматически добавлять extends AnyVal к классам.
Можно написать пару маленьких программок и декомпилировать.
геттеры и сеттеры будут реализованы как методы getVarName() и setVarName(...)

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

Null-safety: интересная штука, но на практике во все публичные методы добавляются проверки для каждого аргумента на null. Мне кажется несколько избыточным — и есть подозрение, что это негативно влияет на производительность. (Там не просто проверка, а вызов статического метода с передачей объекта-параметра и его имени как строки, чтобы можно было кинуть понятное исключение)

Это может и выглядит несколько нагружено, если смотреть в байткоде. Но на деле JVM отлично знает как это оптимизировать. И в рантайме никаких издержек выявить не удалось. Более того, если вы попробуете посмотреть, что получется уже на уровне машинного кода после JIT, то навряд ли найдёте эти проверки в явном виде. Где-то про это даже доклад был на одной из конференций.
А могли бы подсказать где это окно?

Спасибо
Tools -> Kotlin -> Show Kotlin bytecode. Или Ctrl+Shift+A (Cmd+Shift+A) -> "bytecode"
Спасибо!
Я понимаю мотивацию дизайнера языка, который принял решение о таком переопределении операторов. Максимального ограничение возможностей переопределения операторов — это максимальная изоляция программиста от ошибок связанных с такие переопределением, однако минимальнонеобходимая возможность переопределения есть. К тому же это позволяет сохранить достаточно однообразный код.

По поводу объявления метода, то в котлине

fun func():Int = 10
// и
fun func():Int { return 10 }

идентичны. Однако вот такой код

fun func():Int = { 10 }

не скомпилируется, потому что возвращаемое значение будет ()->Int
Единственное различие которое я знаю: если использовать expression вариант, то возвращаемое значение можно не писать, а в блочном варианте компилятор такое уже не пропустит.
Я подразумевал следующее:

fun test(){ println("it works") } 
fun test2() = println("it works too")
fun test3() = {println("surprise!")}

Чтобы вывести "surprise", придётся написать test3()(). Вариант вызова test3() тоже нормально компилируется, только сработает не так, как ожидалось — добавление "лишних" скобочек кардинально меняет логику программы.

Из-за этих граблей переход со скалы на котлин оказался немного болезненным — иногда "по привычке" в объявлении какого-нибудь метода пишу знак равенства, а потом приходится искать ошибки.
Да, я отлично понял о чем вы. Но это актуально только для совпадения двух не очень часто встречающихся факторов: когда функция ничего не возвращает, а вы много лет писали на скала :-)
Не понимаю, зачем нужно переопределять операторы. В С++ этим увлекался, но там есть смысл это делать. А в языках типа джавы — зачем?
Груви — это всё-таки динамический язык, из за этого с котлином их не часто сравнивают, а чаще со скала. Честно говоря, мне не очень нравится дизайн груви, это коненчно субъективно, но всё же. У меня на нем не получается внятно "выразить мысль", часто спотыкаешься о какие-то неприятные мелочи.
Groovy умеет и в статическую типизацию тоже, просто надо объявлять типы. И если взять Groovy со статической типизацией, то он становится очень похож на Kotlin (или Kotlin на него).
У груви есть "строгий режим", да. Но он создает массу ограничений и видно, что приделан сбоку к языку. К тому же я не уверен, но вроде этот режим включается только на уровне каждого отдельного класса, не на уровне компиляции, которая у груви насколько я помню динамическая.
Достаточно посмотреть доклады Евгения Борисова и Баруха Садогруского про Groovy, чтобы понять чтО это за язык. При кажущемся простом и логичном выражении мысли постоянно спотыкаешься о всевозможные грабли.
Кроме того статическая типизация там пятым колесом — все динамически типизировано. А динамика меня угнетает (даже с умной Idea).
А можно где-нибудь эти доклады (или выжимку из них) не посмотреть, а почитать?
Ну, можно попробовать закантактироваться с самими авторами, чтобы прислали или залили куда-нибудь свои презентации. Если Вы пишите на Груви, то посмотреть их тоже не помешает.
Что-то меня пугает синтаксис Конлина. Обилие двоеточий, какие-то стрелочки. Вроде даже символ @ видел в доке. Еще немного и получится руби. Который меня своим синтаксисом в свое время и отвернул от себя в пользу питона :)
Я бы тоже хотел узнать, чем отличается Котлин от Груви, т.к. судя по доке Груви, — он умеет почти все из современных штук-дрюк. ;D
fun passTen(func: (Int)->Int ): ()->Int {
    return { func(10) }
}

Вот такой синтаксис действительно кажется плохо читаемым, но по факту я никаких проблем с "распаршеванием" таких выражений не испытываю. Наверное сказывается некоторая практика, но всё же.
Почти такой же синтаксис сейчас в swift'е и относительно похожий в Rust'е. Это уже своего рода тенденция, как в свое время Java и C# были похожи на С++ и друг на друга. Видимо тут сказывается накопленый опыт в разработке новых языков программирования. Короче говоря, мозг тут придется один раз сломать, но зато после этого ему будут доступны для понимания, по крайней мере, сразу 3 языка программирования.

К тому же в них ещё много общего есть.
Да, в этом смысле котлин хорош тем, что не пытается придумать какой-то свой синтаксис, а максимально использует распространенные способы записи — это сильно упрощает вход, большинство синтаксиса интуитивно понятно для того, кто имел дело с си-подобными языками.
Ну как минимум в Kotlin зачем-то ввели конструкцию fun вместо распространенных def (Python, Ruby, Scala).
С другой стороны, в Swift тоже своя конструкция func, а в Rust так вообще fn :)
Возможно, это было сделано just for fun :)
fun — это из семейства ML'ей.
Меня всегда смущало расположение типа после имени. Тут ради этого применяется еще и дополнительный символ — двоеточие, чтобы как-то разделить. Когда тип указывается до имени, то двоеточия не нужно, читабельность выше. Ты сразу понимаешь с чем работаешь, а потом уже смотришь на имя. А тут ты смотришь на имя, которое по сути вторично, а потом уже понимаешь, какого типа эта переменная, а какой тип возвращает функция. Я понимаю, что дело привычки, но не могу понять, почему так делают :)
Вот на примере того же Груви, я вот щас полистал доку их. Там есть все то же самое, что в Котлине. Но синтаксис кажется чище и менее многословным. Объявление конструктора в продолжении названия класса — для меня вообще дикость :) Т.е. ты перечисляешь в шапке класса его поля в одну строку. В Груви ты просто опускаешь конструктор, но описываешь поля в теле класса, и можешь инициализировать объект, просто передав туда именованные параметры в виде ключ-значение.
Подобные проблемы с читабельностью обычно решаются нормальным форматированием. Например поля класса в Котлине никто в одну строчку не пишет, разбивают на несколько. Код в статьях призван просто демонстрировать принципы, и не всегда похож на "промышленный".
Но все равно шапка класса получается перегруженной. Там итак могут находиться родительский класс, интерфейсы/трейты и т.д., а тут еще и весь конструктор. Сомнительное решение, но я сужу лишь со своей колокольни.
На практике проблем не возникает. Наоборот, библиотека котлина одна из немногих, исходный код которой приятно читать.

Говорю за себя, но Котлин нужно пробовать. Он как хорошие кроссовки — и подошва удобная, и сами не слишком тяжелые. А всевозможные теоретические аргументы за четыре года успели обсудиться, и тот дизай который есть сейчас уже доказал свою оправданность на реальных больших проектах.
При всем уважении, но на чистой java куда больше реальных больших проектов, но это не значит, что текущее состояние языка идеально за счет этого фактора :P

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

Вот вам наглядный пример в коде:
import A.B
import A.C

sealed class A<T> {
    class B<T>(val c: A<T>, val d: A<T>): A<T>() {
        override fun toString(): String = "($c $d)"
    }
    
    class C<T>(val e: T): A<T>() {
        override fun toString(): String = "$e"
    }
}

fun main(args: Array<String>) {
    val f = B(C(1), B(C(2), C(3)))
    println("$f")
}

Очевидно, не правда ли? Куда проще, чем:
import BinaryTree.Node
import BinaryTree.Leaf

sealed class BinaryTree<T> {
    class Node<T>(val left: BinaryTree<T>, val right: BinaryTree<T>): BinaryTree<T>() {
        override fun toString(): String = "($left $right)"
    }
    
    class Leaf<T>(val value: T): BinaryTree<T>() {
        override fun toString(): String = "$value"
    }
}

fun main(args: Array<String>) {
    val tree = Node(Leaf(1), Node(Leaf(2), Leaf(3)))
    println("$tree")
}
А как вы будете делать стул, если вам не важен материал? С металом одна технология работы, а с деревом другая. Метод `add(List items)` сразу говорит нам, что мы добавляем список. Даже не нужно вникать в название параметра. Уже все ясно.
Но дело вкуса, конечно. Не зря же полно популярных языков с обоими стилями.
Когда тип указывается до имени, то двоеточия не нужно, читабельность выше. Ты сразу понимаешь с чем работаешь, а потом уже смотришь на имя.

Но ведь… Разве имя не более точно указывает, с чем работаешь? :)

Синтаксис с типом после имени более удобен тем, что его можно опускать, если в языке есть вывод типов. Опустить тип, который указан перед именем переменной, получается как-то не очень красиво.
Почти везде, где есть вывод типов, используются ключевые слова, вроде def, let, var. В случае Котлина получается val в начале, а тип — в конце. А так был бы только тип вначале :) Целое слово сэкономили. В случае с Груви, мы пишем def, когда тип выводимый, а если нужно строго привести, то можем явно указать тип на месте слова def. Такая запись лично мне кажется более логичной.
Экономии там нет, т.к. идеологи Котлина хотят final везде, где только можно. Иммутабельность рулит. И котлин решает это введением var и val.
final int size = list.size();
val size = list.size();
В таком случае понятна такая идеология.

Статически типизированный код:


class Actor
  def do_it( a, b )
    just_todos = {
      "remark" => ->self.remark(String | Int32),
      "accept" => ->self.accept(String | Int32),
    }
    just_todos[a](b)
  end

  def remark(p)
    puts p
  end
end

Язык?

НЛО прилетело и опубликовало эту надпись здесь

Руби динамический вроде.

Вы что-то конкретное хотели этим сказать?

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

Думаю, в котлине уже есть автоматический вывод типов везде где это нужно, делать это везде где возможно только потому, что это возможно — не котлин вэй. Явные типы параметров и результатов нужны по многим причинам и никуда в котлине они не денутся.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории