Наша команда, аналогично с автором статьи, уже почти как год перешла со Scala на Kotlin в качестве основного языка. Мое мнение во многом совпадает с автором, поэтому предлагаю вам перевод его интересной статьи.
Прошло прилично времени с того момента как я не обновлял блог. Вот уже как год я перешел со Scala, моего основного языка, на Kotlin. Язык позаимствовал много хороших вещей, которые мне нравились в Scala, сумев при этом избежать многих подводных камней и неоднозначности, которая есть в Scala.
Ниже я хочу привести примеры, которые мне нравятся в Scala и Kotlin, а также их сравнение в том, как они реализованы в обоих языках.
Объявление и выведение типов
Что мне особенно нравится в обоих языках так это то, что они оба являются статически типизированными с выведением типов. Это предоставляет вам возможность в полной степени воспользоваться мощью статической типизации без громоздких объявлений в коде (ориг.: declarative boiler plate). В большинстве случаев это работает в обоих языках. В обоих языках также прослеживается предпочтение неизменяемым типам вместе с опциональным объявлением типа переменной после ее названия.
Пример кода будет одинаковый в обоих языках:
Объявление неизменяемой переменной с именем age и типом Int:
val age = 1
Объявление изменяемой переменной с типом String:
var greeting = "Hello"
Оба языка поддерживают лямбда функции как объекты первого класса, которые могут быть присвоены переменным или переданы в качестве параметров функциям:
Scala
val double = (i: Int) => { i * 2 }
Kotlin
val double = {i: Int -> i * 2 }
Data / Case классы
Scala и Kotlin имеют схожий концепт data классов, которые являются представлением data model object.
Подход в Scala
В Scala это case классы, которые выглядят следующим образом:
case class Person(name: String, age: Int)
- Есть apply метод (не нужно использовать ключевое слово new при создание инстанса)
- Методы для доступа объявлены для каждого property (если property объявлено как var то setter метод также будет присутствовать)
- toString, equal и hashCode разумно объявлены
- Eсть copy функция
- Есть unapply метод (который позволяет использовать данные классы в pattern matching)
Подход в Kotlin
Kotlin называет данные классы как data class
data class Person (val name: String, val age: Int)
Ключевые особенности:
- Методы для доступа объявлены для каждого property (если property объявлено как var то setter метод также будет присутствовать). Это не исключительная особенность data классов, утверждение справедливо для любых классов в Kotlin.
- Разумно объявлены toString, equal и hashCode
- Сopy функция
- component1..componentN функции. По аналогии используется в качестве unapply.
- Реализует JavaBean getter и setter, необходимых для таких Java фреймворков как Hibernate, Jackson, без изменений.
В Kotlin нет необходимости в специальном apply методе, также как и не нужно ключевое слово new для инициализации класса. Так что это стандартное объявление конструктора как и для любых других классов.
Сравнение
В основном case и data классы похожи.
Пример ниже выглядит одинаково в обоих языках:
val jack = Person("jack", 1)
val olderJack = jack.copy(age = 2)
В целом я нашел data и case классы взаимозаменяемым в повседневном использовании. В Kotlin есть некоторые ограничения на наследование data классов, но это было сделано из благих намерений c учетом реализации equals и componentN функций, чтобы избежать подводных камней.
В Scala case классы более мощные в pattern matсhing по сравнению с тем как Kotlin работает с data классами в ‘when’ блоках, в которых этого не хватает.
Подход Kotlin работает лучше для существующих Java фреймворков, т.к. они выгдядят для них как обычные Java bean.
Оба языка позволяют передавать параметры по имени и позволяют указать значению по умолчанию для них.
Null Safely / Optionality
Подход в Scala
В Scala null safely заключается в использовании монады option. Проще говоря, option может находится в одном из двух конкретных состояний: Some(x) или None
val anOptionInt: Option[Int] = Some(1)
или
val anOptionInt: Option[Int] = None
Можно оперировать с option при помощи функций isDefined и getOrElse (чтобы указать значение по умолчанию) но более часто используемая ситуация когда монады используется с операторами map, foreach или fold, для которых option представляет из себя коллекцию содержащую 0 или 1 элемент.
Для примера можно подсчитать сумму двух опциональных переменных следующим образом:
val n1Option: Option[Int] = Some(1)
val n2Option: Option[Int] = Some(2)
val sum = for (n1 <- n1Option; n2 <- n2Option) yield {n1 + n2 }
В Переменной sum будет значение Some(3). Наглядный пример того как for может быть использован как foreach или flatMap в зависимости от использования ключевого слова yield.
Другой пример:
case class Person(name: String, age: Option[Int])
val person: Option[Person] = Some(Person("Jack", Some(1)))
for (p <- person; age <- p.age) {
println(s"The person is age $age")
}
Будет напечатана строчка "The person is age 1"
Подход в Kotlin
Kotlin заимствует синтаксис groovy, достаточно практичный в повседневном использовании. В Kotlin все типы non-nullable и должны быть в явном виде объявлены nullable с помощью "?" если они могут содержать null.
Тот же пример может быть переписан следующим образом:
val n1: Int? = 1
val n2: Int? = 2
val sum = if (n1 != null && n2 != null) n1 + n2 else null
Это намного ближе к Java синтаксису за исключением того, что Kotlin принудительно выполняет проверки во время компиляции, запрещая использовать nullable переменные без проверки на null, так что можно не бояться NullPointerException. Также нельзя присвоить null переменной объявленной как non-nullable. Помимо всего компилятор достаточно умный, чтобы избавить от повторной проверки переменной на null, что позволяет избежать многократной проверки переменных как в Java.
Эквивалентный Kotlin код для второго примера будет выглядеть следующим образом:
data class Person(val name: String, val age: Int?)
val person: Person? = Person("Jack", 1)
if (person?.age != null) {
printn("The person is age ${person?.age}")
}
Или альтернативный вариант с использованием "let", который заменает "if" блок на:
person?.age?.let {
person("The person is age $it")
}
Сравнение
Я предпочитаю подход в Kotlin. Он гораздо более читабельный и понятный, и проще разобраться что происходит в многократных вложенных уровнях. Подход Scala отталкивается от поведения монад, который конечно нравится некоторым людям, но по собственному опыту могу сказать, что код становится излишне перегруженным уже для небольших вложений. Существует огромное количество подводных камней у подобного усложнения в использовании map или flatMap, причем вы даже не получите предупреждение при компиляции, если вы делаете что-то не так в мешанине из монад или используя pattern match без поиска альтернативных вариантов, что в результате выливается в runtime exception которые не очевидны.
Подход в Kotlin также уменьшает разрыв при интеграции с кодом Java благодаря тому что типы из нее по умолчанию nullable (тут автор не совсем корректен. Типы из Java попадают в промежуточное состояние между nullable и not-nullable, которое в будущем можно уточнить), тогда как Scala приходится поддерживать null как концепт без null-safely защиты.
Функциональные коллекции
Scala, конечно, поддерживает функциональный подход. Kotlin чуть в меньшей степени, но основные идеи поддерживаются.
В примере ниже нет особых различий в работе fold и map функций:
Scala
val numbers = 1 to 10
val doubles = numbers.map { _ * 2 }
val sumOfSquares = doubles.fold(0) { _ + _ }
Kotlin
val numbers = 1..10
val doubles = numbers.map { it * 2 }
val sumOfSquares = doubles.fold(0) {x,y -> x+y }
Оба языка поддерживают концепт цепочки "ленивых" вычислений. Для примера вывод 10 четных чисел будет выглядеть следующим образом:
Scala
val numbers = Stream.from(1)
val squares = numbers.map { x => x * x }
val evenSquares = squares.filter { _%2 == 0 }
println(evenSquares.take(10).toList)
Kotlin
val numbers = sequence(1) { it + 1 }
val squares = numbers.map { it * it }
val evenSquares = squares.filter { it%2 == 0 }
println(evenSquares.take(10).toList())
Implicit преобразования vs extension методы
Эта та область в которой Scala и Kotlin немного расходятся.
Подход в Scala
В Scala есть концепция implicit преобразований, которая позволяет добавлять расширенный функционал для класса благодаря автоматическому преобразованию к другому классу при необходимости. Пример объявления:
object Helpers {
implicit class IntWithTimes(x: Int) {
def times[A](f: => A): Unit = {
for(i <- 1 to x) {
f
}
}
}
}
Потом в коде можно будет использовать следующим образом :
import Helpers._
5.times(println("Hello"))
Это выведет "Hello" 5 раз. Работает это благодаря тому, что при вызове функции "times" (которая на самом деле не существует в Int) происходит автоматическая упаковка переменной в объект IntWithTimes, в котором и происходит вызов функции.
Подход в Kotlin
Kotlin использует для подобного функционала extension функции. В Kotlin для того, чтобы реализовать подобный функционал нужно объявить обычную функцию, только с префиксом в виде типа, для которая делается расширение.
fun Int.times(f: ()-> Unit) {
for (i in 1..this) {
f()
}
}
5.times { println("Hello")}
Сравнение
Подход Kotlin соответствует тому как я в основном использую данную возможность в Scala, с небольшим преимуществом в виде чуть более упрощенной и понятной записи.
Особенности Scala которых нет в Kotlin и по которым я не буду скучать
Одна из лучших особенностей Kotlin для меня даже не в том функционале что есть, а больше в том функционале которого нет в Kotlin из Scala.
- Вызов по имени — Это разрушает читабельность. Если функция передается было бы гораздо легче увидеть что передается указатель на функции при простом просмотре кода. Я не вижу никаких преимуществ, которое это дает по сравнению с явной передачей лямбд.
- Implicit преобразования — Это то что я действительно ненавижу. Это приводит к ситуации, когда поведение кода значительно меняется в зависимости от того, что было импортировано. В результате действительно тяжело сказать какая переменная будет передана в функцию без хорошей поддержки IDE.
- Перегруженный for — Проблема с несколькими монадами, показанная выше.
- Беспорядок с опциональным синтаксисом infix и postfix операторов — Kotlin чуть более формализованный. В результате код в нем менее двухсмысленный, его проще читать и не так легко простой опечатке стать неочевидной ошибкой.
- Переопределением операторов по максимуму — Kotlin разрешает переопределение только основных операторов (+, — и т.п.). Scala разрешает использовать любую последовательности символов. Действительно ли мне нужно знать разницу между "~%#>" и "~+#>"?
- Медленное время компиляции.
Спасибо за внимание.
Оригинал Scala vs Kotlin
P.S. В некоторые местах в переводе специально оставил слова без перевода (null, null safely, infix, postfix и т.п.).
Автор: nerumb