Как известно, Scala — это мультипарадигменный язык программирования на платформе JVM, в котором чего только не намешано. Главными ингредиентами коктейля являются объектно-ориентированная и функциональная парадигмы, приправленные сверху достаточно изощренной, в сравнении с Java, системой типов. Язык изначально больше похож на исследовательскую лабораторию Мартина Одерски и EPFL, в которой команда, занимающаяся ядром языка, может проводить свои эксперименты и воплощать в жизнь свои самые смелые идеи. И в этом она прекрасна, так как может принести нам множество интересных решений и может стать основой новой ступени эволюции языков программирования. Однако, как мне кажется, именно эта предпосылка и делает Scala не самым лучшем выбором для практичной массовой разработки.

В Scala не чувствуется идеи и единства дизайна, возникает ощущение того, что разработчики взяли кучу классных и, конечно же, крутых возможностей и скатали из них здоровый пластилиновый шар, не задумываясь о том, как это все должно взаимодействовать и сочетаться друг с другом. И как это бывает с пластилином, разделить и достать из такого шара части разных цветов оказывается уже непосильной задачей. Да, иметь сотню тысяч способов сказать что-то на естественном языке, подразумевая, что собеседник из контекста догадается о значении нашей метафоры, — это, конечно, прекрасно, но подобная ситуация в языке программирования приводит к усложнению восприятия кода и неспособности понимать, что вообще происходит и почему ничего не работает так, как было задумано. Становится сложно понять, что является идиоматичным, а что — нет. А причина этого в отсутствии сильного лидера, который четко понимает задачу, которую он пытается решить, видит направление развития языка и вдохновляет своим примером сообщество на новые свершения. В качестве примера можно привести Рича Хики — автора языка Clojure на платформе JVM и технического директора Cognitect.

В Scala-сообществе тоже не все так здорово, как хотелось бы. Существует два непримиримых лагеря, которые не всегда согласны идти на компромиссы. Первый лагерь — люди, тяготеющие к традиционному ООП и испытывающие теплые и нежные чувства к Java, а во втором — люди, пришедшие из различных функциональных языков и пытающиеся сделать из Scala Haskell и сконструировать монаду помощнее и поабстрактнее. У обоих лагерей много всяких классных идей, но, к сожалению, действительно хороших и надежных библиотек с идиоматичным API и хорошей документацией очень немного. В качестве самых ярких примеров таких идей из второго лагеря можно привести библиотеки Scalaz и Shapeless, которые повергают в ужас новичков и людей из первого лагеря. Но их проблема в том, что в Haskell концепция типов по классам является краеугольным камнем и выглядит достаточно просто и идиоматично в отличие от Scala.

«Простота — это высшая утонченность»
Леонардо да Винчи

Scala сложна. И это действительно так, и все это понимают. Scala продолжает свое путешествие по дороге «сложно, но легко» (более подробно смотри здесь) и пытается стыдливо спрятать свои комплексы, ошибки и несовершенства подальше от чужих глаз и притвориться, что этого всего на самом деле нет. Scala очень сильно хочет понравиться нам, разработчикам, и предлагает множество легких в использовании, но очень сложных внутри синтаксических конструкций и сахара. А мы, в свою очередь, падки на такие вещи, так как очень зациклены на своем собственном опыте восприятия кода, чувстве прекрасного и быстром достижении результата. «О, смотри, я набрал всего 16 символов, у меня 20 имплиситов в скоупе, и оно магическим образом работает!»

Мы часто забываем, что работаем для решения чьей-то (заказчика или своей) проблемы и достижения какой-либо цели. И с точки зрения заказчика мы имеем дело с артефактами, мы не поставляем наш исходный код пользователю, и он не смотрит на него со словами: «Ого, линзы, стрелки Клейсли, монадические трансформеры — это реально круто, ребята разбираются в теории категорий». Производительность, приспосабливаемость к изменениям и вообще-то, как оно работает, — это все атрибуты собранного артефакта, а не исходных синтаксических конструкций. И нам важно понимать:

  • Делает ли программное обеспечение то, что должно?
  • Насколько оно качественно?
  • Можем ли мы положиться на него в том, что оно должно делать?
  • Сможем ли мы исправить проблемы, когда они появятся?
  • Если появятся новые требования, сможем ли мы изменить нашу систему?

Эти вещи слабо связаны с самими конструкциями, которые мы набираем на клавиатуре и имеют непосредственное отношение к артефакту. Мы должны начать оценивать конструкции на основе характеристик артефактов, которые они производят.

Перейдем на личности. Пауль Филлипс — человек, который потратил на Scala более 5 лет своей жизни, написал кода больше, чем кто-либо еще, и внес огромный вклад в реализацию компилятора Scala. В 2013 году он сорвался и сказал, что выходит из игры, так как не может больше мириться с тем, что происходит, о чем в красках рассказывал на нескольких конференциях. Рассмотрим же те конкретные примеры, о которых он говорит.

«Когда я работаю над задачей, я никогда не задумываюсь о красоте. Я размышляю только о том, как ее решить.»
«Но если по завершении работы я вижу, что решение некрасиво, то я знаю, что оно ошибочно.»
Р. Бакминстер Фуллер

Наше утонченное чувство прекрасного всегда готово подсказать нам то, какие решения являются некрасивыми, сложными и, как говорится в цитате, могут быть ошибочными.

trait ParMapLike[K,
                 V,
                 +Repr <: ParMapLike[K, V, Repr, Sequential] with ParMap[K, V],
                 +Sequential <: scala.collection.mutable.Map[K, V] with scala.collection.mutable.MapLike[K, V, Sequential]]
extends scala.collection.GenMapLike[K, V, Repr]
   with scala.collection.parallel.ParMapLike[K, V, Repr, Sequential]
   with Growable[(K, V)]
   with Shrinkable[K]
   with Cloneable[Repr]

Знакомьтесь, ParMapLike из стандартной библиотеки коллекций — интерфейс с 4 параметрами типов, вариативностью и сложным унаследованным поведением. Даже опытному разработчику достаточно сложно понять, что же делает этот интерфейс. И это весьма нередкая ситуация, многие интерфейсы стандартной библиотеки жестко завязаны на специфику их реализации и светят её наружу, что, в свою очередь, убивает переиспользуемость кода на корню. Наш прекрасный и утонченный мир монад и категорий эндофункторов начинает потихоньку превращаться в зеркальный лабиринт минотавра, выбраться из которого уже не представляется возможным.

Вообще библиотека коллекций вобрала в себя достаточно спорные решения, и главное из них — это попытка построения абстракций над изменяемыми и неизменяемыми коллекциями через наследование. Унаследованные реализации всегда где-нибудь оказываются некорректными. Например, как написать метод drop так, чтобы он был переиспользуемым для всех типов mutable и immutable коллекций? В итоге, чтобы быть уверенным в корректности, необходимо всегда проверять и переопределять унаследованные методы. Но зачем тогда вообще нужно было вводить абстракции посредством такого наследования? Целая куча багов в библиотеке коллекций вызвана именно тем, что реализация метода по умолчанию, провалившаяся сверху, оказывается некорректной в случае конкретного класса коллекции.

А теперь присмотримся ко всеобщей любимице, палочке-выручалочке, которая берет наш замечательный контейнер, например Map, применяет функцию f к каждой паре ключ-значение и возвращает нам Map с новыми значениями новых типов. Простая и понятная операция, а вот и её сигнатура из официального scaladoc:

def map[B](f: (A) => B): Map[B]

однако, это неправда, и у функции есть еще и так называемая «полная» сигнатура:

def map[B, That](f: ((A, B)) => B)(implicit bf: CanBuildFrom[Map[A, B], B, That]): That

в которой мы вместо двух — видим уже три параметра типов. Ан нет, четыре параметра типов, тип функции f с легкостью вводит в заблуждение. Корректнее было бы записать сигнатуру в следующем виде:

def map[B, That](f: ((K, V)) => B)(implicit bf: CanBuildFrom[Map[K, V], B, That]): That

Получается, что доверять официальной документации можно только на свой страх и риск. Доверяй, но проверяй. Нас сознательно вводят в заблуждение упрощенной версией сигнатуры, пытаясь скрыть сложность конструкции, опуская неявный (implicit) параметр.

Первый вариант с упрощенной сигнатурой — это одна из самых мощных, гибких и переиспользуемымих абстракций, которая проста для понимания и легка в обращении. Но вариант с полной сигнатурой таковым уже не является. И вот к чему это приводит:

// Вроде бы все в порядке
scala> BitSet(1, 2, 3) map (_.toString.toInt)
res0: BitSet = BitSet(1, 2, 3)

// Хм, а вот это уже что-то странное
scala> BitSet(1, 2, 3) map (_.toString) map (_.toInt)
res1: SortedSet[Int] = TreeSet(1, 2, 3)

Нельзя просто так взять и сделать декомпозицию операции, сохранив уверенность в типе возвращаемого значения. И вот еще один замечательный пример:

scala> def f[T](x: T) = (x, new Object)
F: [t](x: T)(T, Object)

scala> SortedSet(1 to 10: _*)
res0: SortedSet[Int] = TreeSet(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

scala> SortedSet(1 to 10: _*) map (x => f(x)._1)
res1: SortedSet[Int] = TreeSet(1, 2, 3 ,4 ,5, 6, 7, 8, 9, 10)

// А как же порядок?
scala> SortedSet(1 to 10: _*) map f map (_._1)
res2: Set[Int] = Set(5, 1, 6, 9, 10, 2, 7, 4, 8, 3)

В случае последовательного применения map компилятор просто забывает исходный тип, и на выходе мы получаем уже совершенно не то, что ожидали — обычный неупорядоченный Set.

«Компилятор Scala — крайне сложная штука, для того, чтобы внести в него какие-либо изменения, нужно быть специалистом по ракетостроению.»
«Если ты не можешь внести изменения, то и исправить ты уже ничего не можешь.»
Пауль Филлипс

Scala гордится своей системой типов и типобезопасностью, пытается встать в один ряд с признанным мастером этого ремесла — Haskell-ем, однако, из-за хитросплетений реализации и сопутствующих им недостатков и багов, существует множество примеров, когда на деле Scala оказывается не такой уж безопасной и совсем уж запутанной. На лицо явно некорректное и непредсказуемое для нас поведение:

scala> List(1, 2, 3).toSet()
res0: Boolean = false

scala> List(1, 2, 3) contains "sweetroll"
res2: Boolean = false

scala> val x1: Float = Long.MaxValue
x1: Float = 9.223372E18

scala> val x2: Float = Long.MaxValue - Int.MaxValue
x2: Float = 9.223372E18

scala> println(x1 == x2)
true

Да, Scala очень любит недосказанности и проворачивать свои всякие неявные дела, пока мы этого не видим. Автоматические неявные приведения типов, постоянные обобщения типов до общего типа Any, что эквивалентно динамической типизации. Все это просто не оставляет нам шансов на то, чтобы понять, что происходит и удержать это в голове. В связи с чем возникает вопрос: как после этого вообще можно быть уверенным в корректности чего-либо?

def compare(x: T, y: T): Int

Идем дальше, у нас в распоряжении богатая и мощная система типов, которая стремится к тому, чтобы можно было описывать бизнес-логику типами и доказывать её корректность на уровне компилятора. Но вместо этого функция сравнения двух объектов возвращает не красивое значение с тремя состояниями, а безликий Int с 4294967296 состояниями, не отражающими смысл результата.

Функция equals — пример и причина полнейшего пренебрежения типобезопасностью при использовании оператора «==», паттерн-матчинге и работе с коллекциями. В библиотеке Scalaz по такому случаю специально вводится оператор «===» типобезопасного сравнения.

def equals(x: Any): Boolean

case class Pos(x: Int, y: Int)
case class Cube(pos: Pos)

val startPos = new Pos(0, 0)
val cubeOnStart = new Cube(startPos)

if (startPos == cubeOnStart) {
    // Этот код никогда не будет выполнен, но компиляцию он проходит без каких-либо проблем
}

И напоследок, из-за решений дизайна функция filter всегда возвращает новую коллекцию, даже если никаких изменений с ней не произошло. Коллекция из огромного числа элементов просто будет скопирована, и очевидная оптимизация «вернуть оригинальную коллекцию» в данном случае не реализуема без глобального рефакторинга компилятора.

(1 to 10000000000).toList filter (_ => true)

И это все является как проблемами дизайна языка и его библиотек, так и проблемами их реализации. Да, можно сказать, что все это спрятано от наших глаз и 90% времени мы этого всего не замечаем, тем не менее, все это оказывает огромное влияние на развитие всей дальнейшей экосистемы языка. И тысячи или миллионы багов возникают из-за хитросплетений состояния, времени и чрезмерной скрытой сложности используемых абстракций. Кроме того, очень часто возникает ситуация, когда в принципе сложно понять: это баг или ожидаемое поведение Scala. В итоге разработчик оказывается в ловушке из собственноручно расставленных капканов избыточной выразительности.

И все это приводит к следующей мысли: как можно сделать корректной и надежной систему, которую ты не понимаешь? Это очень и очень сложно. По мере того, как мы делаем системы более гибкими и расширяемыми, мы идем на компромисс с тем, что в итоге перестаем понимать их поведение. Но в случае, когда мы хотим быть уверенными в корректности, мы должны ограничиться тем, что мы можем понять и что доступно для нашего понимания. А наше понимание весьма ограничено, как количество мячиков, которыми мы можем одновременно жонглировать. Это конечное число, и оно весьма невелико. Так что мы можем рассматривать за раз лишь малое число вещей, и если эти вещи переплетены и связаны друг с другом, то мы теряем возможность рассматривать их по отдельности. Такова природа связанности, и каждое такое связывание подбрасывает нам в руки еще один мячик. Scala же не дает нам спуску и постоянно продолжает подбрасывать нам мячи до тех пор, пока мы не начнем их ронять.

«Простота — необходимое условие надежности»
Эдсгер Вайб Дейкстра

И похоже, что люди начали уставать от сложившейся ситуации, и в настоящее время существует тенденция ухода от Scala, например Typesafe — основная компания, непосредственно стоящая за продвижением, развитием экосистемы языка и зарабатывающая на нем деньги, недавно объявила о своем переименовании в Lightbend и смещении акцента со Scala на Java. Lightbend принял решение доработать Java-интерфейс своих основных детищ и попробовать занять более уверенную позицию в сфере enterprise-разработки, где водятся деньги и балом правит Java. Похоже на то, что такие крупные технологии как Spark, Kafka, Akka будут в дальнейшем развивать Java API не хуже, чем текущий Scala API. Возможно, здесь все-таки сыграл свою роль революционный для мира Java релиз 8 версии. В Java снова закипела жизнь, и оказалось, что и с ней теперь можно быть более продуктивным.

Scala полна классных задумок, интересных нововведений и экспериментов. Она может быть хороша для небольшой, слаженной команды специалистов, которые хорошо ладят друг с другом и которые готовы мириться со всеми её недостатками. Но для условий корпоративной разработки Scala подходит не очень хорошо, потому что чрезмерно сложна и непрактична для разработчиков, которые каждый день решают свои бизнес-задачи в условиях огромных команд и больших систем и которым нужен мощный, но в то же время простой инструмент для работы.

Вадим Дубс

RSS