Scala vs Kotlin (переклад)

Наша команда, аналогічно з автором статті, вже майже як рік перейшла зі Scala на Kotlin в якості основного мови. Моя думка багато в чому збігається з автором, тому пропоную вам переклад його цікавої статті.
пристойно часу Пройшло з того моменту як я не оновлював блог. Ось вже як рік я перейшов з Scala, мого основного мови, на Kotlin. Мова запозичив багато хороших речей, які мені подобалися в Scala, зумівши при цьому уникнути багатьох підводних каменів і неоднозначності, яка є в Scala.
Нижче я хочу привести приклади, які мені подобаються в Scala і Kotlin, а також їх порівняння у тому, як вони реалізовані в обох мовах.

Оголошення і виведення типів

Що мені особливо подобається в обох мовах так це те, що вони обидва є статично типізованими з виведенням типів. Це надає вам можливість повною мірою скористатися міццю статичної типізації без громіздких оголошень в коді (ориг.: declarative boiler plate). У більшості випадків це працює в обох мовах. В обох мовах також простежується перевагу незмінним типам разом з опціональним оголошенням типу змінної після її назви.
Приклад коду буде однаковий в обох мовах:
Оголошення незмінною змінної з ім'ям age і типом Int:
val age = 1 

Оголошення змінною змінною з типом String:
var greeting = "Привіт"

Обидві мови підтримують лямбда функції як об'єкти першого класу, які можуть бути присвоєні змінним або передані в якості параметрів функцій:
Scala
val double = (i: Int) = { i * 2 }

Kotlin
val double = {i: Int -> i * 2 }

Data / Case класи

Scala і Kotlin мають схожий концепт data класів, які є представленням data object model.
Підхід в Scala
В Scala це case класи, які виглядають наступним чином:
case class Person(name: String, age: Int)

  • apply метод (не потрібно використовувати ключове слово new при створення инстанса)
  • Методи для доступу оголошені для кожного property (якщо property оголошено var setter метод також буде присутній)
  • toString, equal hashCode розумно оголошені
  • Є функція 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
  • Сору функція
  • 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 класів, але це було зроблено з благих намірів з урахуванням реалізації дорівнює 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 <1 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, причому ви навіть не отримаєте попередження при компіляції, якщо ви робите щось не так в мішанині з монад або використовуючи match pattern без пошуку альтернативних варіантів, що в результаті виливається в 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) { _ + _ }

Kotin
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 перетворень, яка дозволяє додавати розширений функціонал для класу завдяки автоматичному перетворенню до іншого класу при необхідності. Приклад оголошення:
Helper object {
implicit class IntWithTimes(x: Int) {
def times[A](f: => A): Unit = {
for(i <- 1 to x) { 
f 
}
}
}
}

Потім в коді можна буде використовувати наступним чином :
import Helpers._
5.times(println("Привіт"))

Це виведе "Привіт" 5 раз. Працює це завдяки тому, що при виклику функції "times" (яка насправді не існує в Int) відбувається автоматична упаковка змінної в об'єкт IntWithTimes, в якому і відбувається виклик функції.
Підхід в Kotlin
Kotlin використовує для подібного функціоналу extension функції. У Kotlin для того, щоб реалізувати подібний функціонал потрібно оголосити звичайну функцію, тільки з префіксом у вигляді типу, яка робиться розширення.
fun Int.times(f: ()-> Unit) {
for (i in 1..this) {
f()
}
}

5.times { println("Привіт")}

Порівняння

Підхід 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 і тощо).

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.