Споры насчет преимуществ и недостатков Scala перед Java напоминают мне споры о C против С++. Плюсы, конечно же, на порядок более сложный язык с огромным количеством способов выстрелить себе в ногу, уронить приложение или написать совершенно нечитабельный код. Но, с другой стороны, C++ проще. Он позволяет делать простым то, что на голом C было бы сложно. В этой статье я попытаюсь рассказать о той стороне Scala, которая сделала этот язык промышленным — о том, что делает программирование проще, а исходный код понятнее.
Дальнейшее сравнение между языками исходит из того, что читатель знаком со следующими вещами:
— Java8. Без поддержки лямбд и говорить не о чем
— Lombok Короткие аннотации вместо длинных простыней геттеров, сеттеров, конструкторов и билдеров
— Guava Иммутабельные коллекции и трансформации
— Java Stream API
— Приличный фреймворк для SQL, так что поддержка multiline strings не так и нужна
— flatMap — map, заменяющий элемент на произвольное количество (0, 1, n) других элементов.
Иммутабельность по умолчанию.
Наверное, все уже согласны, что иммутабельные структуры данных — это Хорошая Идея. Scala позволяет писать иммутабельный код, не расставляя `final`
@Data class Model { private final String s; private final 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 = ??? //объявляем локальную переменную типа 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 } //если вдруг этот матч не охватывает все возможные значения, компилятор выдаст предупреждение
Набор синтаксических фич для поддержки композиции
Если первыми тремя китами ООП являются (говорим хором) инакпсуляция, полиморфизм и наследование, а четвертым агрегация, то пятым китом, несомненно, станет композиция функций, лямбд и объектов.
В чем тут проблема джавы? В круглых скобочках. Если не хочется писать однострочники, то при вызове метода с лямбдой придется заворачивать её дополнительно в круглые скобки вызова метода.
//допустим у нас есть библиотека иммутабельных коллекций с методами 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 и ФП в целом. И ничего не сказано о недостатках (у кого их нет…). Но я надеюсь, что джависты получат ответ на вопрос «Зачем нужна эта скала», а скалисты смогут лучше отстаивать честь своего языка в сетевых баталиях 🙂
ссылка на оригинал статьи https://habrahabr.ru/post/315050/
Добавить комментарий