No way back: Чому я перейшов з Java на Scala і не збираюся повертатися

Спори щодо переваг і недоліків Scala перед Java нагадують мені спори про C проти С++. Плюси, звичайно ж, на порядок більш складний мову з величезною кількістю способів вистрілити собі в ногу, опустити додаток або написати абсолютно нечитабельний код. Але, з іншого боку, C++ простіше. Він дозволяє робити простим те, що на голому C було б складно. У цій статті я спробую розповісти про тій стороні Scala, яка зробила цю мову промисловим — про те, що робить програмування простіше, а вихідний код зрозуміліше.



Подальше порівняння між мовами виходить з того, що читач знайомий з наступними речами:
— Java8. Без підтримки лямбд і говорити не про що
Lombok Короткі анотації замість довгих простирадлом геттеров, сеттерів, конструкторів і білдер
Guava Иммутабельные колекції і трансформації
Java Stream API
— Пристойний фреймворк для SQL, так що підтримка multiline strings не так і потрібна
flatMap — map, замінює елемент на довільну кількість (0, 1, n) інших елементів.

Иммутабельность за замовчуванням.
Напевно, всі вже згодні, що иммутабельные структури даних — це Хороша Ідея. Scala дозволяє писати иммутабельный код, не розставляючи `final`

Java
@Value
class Model {
String s;
int i;
}
public void method(final String a, final int b) {
final String c = a + b;
}



Scala
case class Model(s: String; i: Int)
def method(a: String, b: Int): Unit = {
val c: String = a + b
}



Блок коду, умова, switch є виразом, а не оператором
Тобто все перераховане вище повертає значення, дозволяючи позбавитися від оператора return і істотно спрощуючи код, який працює з иммутабельными даними або великою кількістю лямбд.
Java

final String s;
if (condition) {
doSomething();
s = "так";
} else {
doSomethingElse();
s = "ні"
}



Scala
val s = if (condition) {
doSomething();
"так"
} else {
doSomethingElse();
"ні"
}



Pattern matching, unapply() і sealed class hierarchies
Ви коли-небудь хотіли мати switch, працює з довільними типами даних, що видає попередження при компіляції, якщо він охоплює не всі можливі випадки, а також вміє робити вибірки за складних умов, а не по полях об'єкта? В Scala він є!
Scala
sealed trait Shape //sealed trait - інтерфейс, всі реалізації якого повинні бути оголошені в цьому файлі
case class Dot(x: Int, y: Int) extends Shape
case class Circle(x: Int, y: Int, radius: Int) extends Shape
case class Square(x1: Int, y1: Int, x2: Int, y2: Int) extends Shape

val shape: Shape = ??? //оголошуємо локальну змінну типу Shape

val description = shape match {
//x і x в виразі нижче - це поля об'єкта Dot
case Dot(x, y) => s"dot(" + x + ", " + y + ")"
//Circle, у якого радіус дорівнює нулю. А також форматування рядків в стилі Scala
case Circle(x, y, 0) => s"dot($x, $y)"
//якщо радіус менше 10
case Circle(x, y, r) if r < 10 => s"smallCircle($x, $y, $r)"
case Circle(x, y, radius) => s"circle($x, $y, $radius)"
//а прямокутник ми вибираємо явно за типом
case sq: Square => "random square: " + sq.toString
} //якщо раптом цей матч не охоплює всі можливі значення, компілятор видасть попередження


JavaНавіть намагатися не буду повторити це на джаві.


Набір синтаксичних функцій для підтримки композиції
Якщо першими трьома китами ООП є (говоримо хором) инакпсуляция, поліморфізм і наслідування, а четвертим агрегація, то п'ятим китом, безсумнівно, стане композиція функцій, лямбд і об'єктів.
В чому тут проблема джави? В круглих дужках. Якщо не хочеться писати однострочники, то при виклику методу з лямбдой доведеться загортати її додатково в круглі дужки виклику методу.
Java

//припустимо у нас є бібліотека иммутабельных колекцій з методами map і flatMap. Для іншої бібліотеки колекцій це буде ще більше коду.
//у collection замінити кожен елемент на нуль, один чи декілька інших елементів, обчислюваних за алгоритмом
collection.flatMap(e -> {
return getReplacementList(e).map(e -> {
int a = calc1(e);
int b = calc2(e);
return a + b;
});
});

withLogging("my operation {} {}", a, b, () => {
//do something
});


Scala
collection.flatMap { e =>
getReplacementList(e).map { e =>
val a = calc1(e)
val b = calc2(e)
a + b
}
}

withLogging("my operation {} {}", a, b) {
//do something
}


Різниця може здаватися незначною, але при масовому використанні лямбд вона стає істотною. Приблизно як використання лямбд замість inner classes. Звичайно, це вимагає наявності відповідних бібліотек, розрахованих на масове використання лямбд — але вони, безсумнівно, вже є або незабаром з'являться.

Параметри методів: іменовані параметри за замовчуванням
Scala дозволяє явно вказувати назви аргументів при виклику методів, а також підтримує значення аргументів за замовчуванням. Ви коли-небудь писали конвертори між доменними моделями? Ось так це виглядає в скелі:
Scala
def convert(do: PersonDataObject): Person = {
Person(
firstName = do.name,
lastName = do.surname,
birthDate = do.birthDate,
address = Address(
city = do.address.cityShort,
street = do.address.street
)
) 


Набір параметрів і їх типи контролюються на етапі компіляції, в рантайме це просто виклик конструктора. В джаві ж доводиться використовувати або виклик конструктора/фабричного методу (відсутність контролю за аргументами, переплутав місцями два строкових аргументу і привіт), або білдери (майже добре, але те, що при конструюванні об'єкта були вказані всі потрібні параметри, можна перевірити тільки в рантайме).

null NullPointerException
Скаловский `Option` принципово нічим не відрізняється від джавового `Додаткова`, але перераховані вище особливості роблять роботу з ним легкою і приємною, в той час як в джаві доводиться докладати певні зусилля. Програмістам на скелі не потрібно змушувати себе уникати nullable полів — клас-обгортка не менш зручний, ніж null.
Scala
val value = optValue.getOrElse("no value") //значення або рядок "no value"
val value2 = optValue.getOrElse { //значення або exception
throw new RuntimeException("value is missing")
}
val optValue2 = optValue.map(v => "The " + v) //Option("The " + value)
val optValue3 = optValue.map("The " + _) //те ж саме, скорочена форма
val sumOpt = opt1.flatMap(v1 => opt2.map(v2 => v1 + v2)) //Option від суми значень двох інших Option

val valueStr = optValue match { //Option - це теж sealed trait з двома нащадками!

case Some(v) => //зробити що-то якщо є значення, повернути рядок
log.info("we got value {}", v)
"value.toString is " + v

case None => //зробити що-то якщо немає значення, повернути іншу рядок
log.info("we got no value")
"no value"
}



Звичайно ж, цей список не повний. Більше того, кожен приклад може здатися незначущим — ну, яка, насправді, різниця, скільки скобочек доведеться написати при виклику лямбды? Але ключова перевага скелі — це код, який виходить в результаті комбінування всього вищепереліченого. Так java5 від java8 не дуже відрізняється в плані синтаксису, але набір дрібних змін робить розробку істотно простіше, в тому числі відкриваючи нові можливості в архітектурному плані.

Також ця стаття не висвітлює інші потужні (і небезпечні) фічі мови, екосистему Scala і ФП в цілому. І нічого не сказано про недоліки (у кого їх немає...). Але я сподіваюся, що джависты отримають відповідь на питання «Навіщо потрібна ця скеля», а скелясті зможуть краще відстоювати честь своєї мови в мережевих баталіях :-)
Джерело: Хабрахабр

0 коментарів

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