No way back: Почему я перешел с Java на Scala и не собираюсь возвращаться

в 17:49, , рубрики: holywar, 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 = "yes";
} else {
  doSomethingElse();
  s = "no"
}

Scala

val s = if (condition) {
  doSomething();
  "yes"
} else {
  doSomethingElse();
  "no"
}

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 = 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
  } //если вдруг этот матч не охватывает все возможные значения, компилятор выдаст предупреждение

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` принципиально ничем не отличается от джавового `Optional`, но вышеперечисленные особенности делают работу с ним легкой и приятной, в то время как в джаве приходится прилагать определенные усилия. Программистам на скале не нужно заставлять себя избегать 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 и ФП в целом. И ничего не сказано о недостатках (у кого их нет...). Но я надеюсь, что джависты получат ответ на вопрос «Зачем нужна эта скала», а скалисты смогут лучше отстаивать честь своего языка в сетевых баталиях )

Автор: Scf

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js