Споры насчет преимуществ и недостатков Scala перед Java напоминают мне споры о C против С++. Плюсы, конечно же, на порядок более сложный язык с огромным количеством способов выстрелить себе в ногу, уронить приложение или написать совершенно нечитабельный код. Но, с другой стороны, C++ проще. Он позволяет делать простым то, что на голом C было бы сложно. В этой статье я попытаюсь рассказать о той стороне Scala, которая сделала этот язык промышленным — о том, что делает программирование проще, а исходный код понятнее.
Дальнейшее сравнение между языками исходит из того, что читатель знаком со следующими вещами:
— Java8. Без поддержки лямбд и говорить не о чем
— Lombok Короткие аннотации вместо длинных простыней геттеров, сеттеров, конструкторов и билдеров
— Guava Иммутабельные коллекции и трансформации
— Java Stream API
— Приличный фреймворк для SQL, так что поддержка multiline strings не так и нужна
— flatMap — map, заменяющий элемент на произвольное количество (0, 1, n) других элементов.
Иммутабельность по умолчанию
Наверное, все уже согласны, что иммутабельные структуры данных — это Хорошая Идея. Scala позволяет писать иммутабельный код, не расставляя `final`
@Value
class Model {
String s;
int i;
}
public void method(final String a, final int b) {
final String c = a + b;
}
case class Model(s: String, i: Int)
def method(a: String, b: Int): Unit = {
val c: String = a + b
}
Блок кода, условие, switch являются выражением, а не оператором
Т.е. всё вышеперечисленное возвращает значение, позволяя избавиться от оператора return и существенно упрощая код, работающий с иммутабельными данными или большим количеством лямбд.
final String s;
if (condition) {
doSomething();
s = "yes";
} else {
doSomethingElse();
s = "no"
}
val s = if (condition) {
doSomething();
"yes"
} else {
doSomethingElse();
"no"
}
Pattern matching, unapply() и sealed class hierarchies
Вы когда-нибудь хотели иметь switch, работающий с произвольными типами данных, выдающий предупреждение при компиляции, если он охватывает не все возможные случаи, а также умеющий делать выборки по сложным условиям, а не по полям объекта? В 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 = getSomeShape() //объявляем локальную переменную типа Shape
val description = shape match {
//x и x в выражении ниже - это поля объекта Dot
case Dot(x, y) => "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
} //если вдруг этот матч не охватывает все возможные значения, компилятор выдаст предупреждение
Набор синтаксических фич для поддержки композиции
Если первыми тремя китами ООП являются (говорим хором) инакпсуляция, полиморфизм и наследование, а четвертым агрегация, то пятым китом, несомненно, станет композиция функций, лямбд и объектов.
В чем тут проблема джавы? В круглых скобочках. Если не хочется писать однострочники, то при вызове метода с лямбдой придется заворачивать её дополнительно в круглые скобки вызова метода.
//допустим у нас есть библиотека иммутабельных коллекций с методами 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
});
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 позволяет явно указывать названия аргументов при вызове методов, а также поддерживает значения аргументов по умолчанию. Вы когда-нибудь писали конверторы между доменными моделями? Вот так это выглядит в скале:
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` принципиально ничем не отличается от джавового `Optional`, но вышеперечисленные особенности делают работу с ним легкой и приятной, в то время как в джаве приходится прилагать определенные усилия. Программистам на скале не нужно заставлять себя избегать nullable полей — класс-обертка не менее удобен, чем null.
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 и ФП в целом. И ничего не сказано о недостатках (у кого их нет...). Но я надеюсь, что джависты получат ответ на вопрос «Зачем нужна эта скала», а скалисты смогут лучше отстаивать честь своего языка в сетевых баталиях )
Автор: Scf