Pull to refresh
894.23
OTUS
Цифровые навыки от ведущих экспертов

Scala 3 / Dotty – Факты и Мнения. Что мы ожидаем?

Reading time13 min
Views6.9K
Original author: EMANUEL OLIVEIRA

Привет, Хабр. Для будущих студентов курса «Scala-разработчик» подготовили перевод материала.

Приглашаем также на открытый вебинар «Эффекты в Scala». Участники вместе с экспертом рассмотрят понятие эффекта и сложности, которые могут возникать при их наличии, а также рассмотрят понятие функционального эффекта и его свойства.


Что такое Scala 3?

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

Что мотивировало появление новой версии, которая связана с самой сутью Scala (а именно DOT-вычисления — причина, по которой Scala 3 начиналась как Dotty); в новой версии наблюдается повышение производительности и предсказуемости, что делает код более легким, интересным и безопасным; улучшение инструментария и бинарной совместимости; а также еще более дружелюбное отношение к новичкам.

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

Scala 3 — новый язык?

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

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

Почему происходит столь много изменений одновременно?

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

Scala 3 — это новый Python 3?

Существует необоснованное убеждение, что Scala 3 — это новый Python 3 относительно его совместимости с предыдущей версией. Однако, есть некоторые аргументы против этого мнения: а именно, что вам не нужно переносить все на Scala 3, так как есть бинарная совместимость с Scala 2.13 (подробнее об этом будет в разделе о миграции); вы можете уверенно мигрировать благодаря сильной системе типа Scala; и есть гораздо больше преимуществ при миграции с 2 на 3 в Scala, чем было бы при миграции с 2 на 3 на Python 3.

Какие изменения ключевые?

Мы выбрали некоторые ключевые функции, которые считаем более актуальными для начинающих программистов Scala. Мы их опишем и прокомментируем то, как они могут на нас повлиять. Мы не будем комментировать все новые возможности, потому что список слишком длинный. В любом случае, эта статья не будет учебным пособием по каждой функции. Если вы хотите увидеть список всех изменений, ссылок и других ресурсов, вы можете посмотреть это на dotty.epfl.ch.

Optional Braces (опциональные или необязательные фигурные скобки)

Одной из самых революционных новинок являются optional braces и использование правил отступа, как в случае с Python. Это действительно революционно, потому что визуально меняется код и это влияет на читабельность — достаточно, чтобы невнимательный читатель подумал, что это новый язык. В дополнение к тому, что это приводит к более «чистому и короткому коду». Optional braces — хорошая вещь, потому что: 

  1. Мы зачастую пытаемся опустить фигурные скобки там, где это возможно (например, для методов/функций, которые состоят из одного выражения);

  2. Даже в случае со скобками почти каждый проект использует некоторые правила для отступов очень строго (это проверяется либо при просмотре кода, либо при принудительном применении scalafmt), так что скобки являются для нас лишь дополнительным токеном для регистрации во время чтения кода, и это не вводит никакой дополнительной информации.)

trait Printer:
  def print(msg: String): Unit

class ConsolePrinter extends Printer:
  def print(msg: String): Unit = println(msg)

class EmojiPrinter(underlying: Printer) extends Printer:
  def print(msg: String): Unit =
    val emoji = msg match
      case ":)"  => ?
      case ":D"  => ?
      case ":|"  => ?
      case other => other
    underlying.print(emoji)

Одним из недостатков использования правил отступов является распознавание того момента, когда заканчивается большая область отступов. Для решения этой проблемы Scala 3 предлагает end маркер. 

class EmojiPrinter(underlying: Printer) extends Printer:
  def print(msg: String): Unit =
    if msg != null then
      val emoji = msg match
        case ":)"  => ?
        case ":D"  => ?
        case ":|"  => ?
        case other => other
      underlying.print(emoji)
    end if
end EmojiPrinter

Обратите внимание, что мы не ставим скобки, а также обратите внимание на then. 

Оба эти — и другие изменения — являются частью нового синтаксиса управления — еще одно изменение, которое визуально влияет на код.

Помимо увеличения кодовой базы по сравнению с опцией работы со скобками, преимущество end маркеров по сравнению со скобками заключается в том, что вы можете маркировать замыкание. Таким образом, становится легче найти тот компонент, который замыкается.

Не рекомендуется повсеместно ставить end маркер. Подсказка такая — использовать его, когда область отступов слишком длинная. Однако, определение «слишком длинный» может варьироваться от человека к человеку. Таким образом, в соответствии с официальной документацией, end маркер имеет смысл если:

  • Конструктор содержит пустые строки, или

  • Конструктор имеет 15 строк и более

  • Конструктор имеет 4 уровня отступов и более

Согласно официальной документации, опциональные скобки являются экспериментальной функцией, т.е. их можно деактивировать. Кроме того, компилятор предупредит о любом плохо откорректированном коде. Более того, правило заключается в том, что добавление пары опциональных скобок не изменит смысла хорошо продуманной программы.

Enums

Почти каждый программист Scala, работающий на Java, пропускает ключевое слово enum и концепцию, которую он воплощает. Представим, что до Scala 3 вам нужно было написать какой-нибудь шаблонный код, чтобы достичь чего-то похожего на перечисление:

sealed trait Color
case object Red extends Color
case object Green extends Color
case object Blue extends Color

В Scala 3, мы можем использовать стандартные типы enum:

enum Color:
  case Red, Blue, Green

С годами все больше и больше код пишется с учетом безопасности типа. Такие концепции, как алгебраические типы данных (ADT), стали обычным явлением в системном моделировании. Поэтому было бы целесообразно предложить программистам более простой механизм реализации этих структур данных. Действительно, Scala 3 предлагает более простой способ реализации ADT через enums:

enum Option[+T]:
  case Some(x: T) // extends Option[T]       (omitted)
  case None       // extends Option[Nothing] (omitted)

Если вы хотите сделать ваш определенный Scala-enum совместимым с Java-enum, вам необходимо расширить java.lang.Enum, который импортируется по умолчанию:

enum Color extends Enum[Color]:
  case Red, Blue, Green

println(Color.Green.compareTo(Color.Red)) // 2

Если вам интересен более сложный случай перечисления, например, с параметрами или обобщенными ADT, посмотрите на ссылку enums.

Редизайн implicit (неявность)

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

Implicit определения → Заданные экземпляры

В данном случае речь идет о том, как Scala 3 использует синтез контекстных параметров для определенного типа. Она заменяет предыдущее implicit использование для этой цели. В Scala 3 вы можете дополнительно указывать имя заданного экземпляра. Если вы пропустите это имя, то компилятор выведет его.

trait Ord[T]:
  def compare(a: T, b: T): Int

given intOrd: Ord[Int] with // with name
  def compare(a: Int, b: Int): Int = a - b

given Order[String] with // without name
  def compare(a: String, b: String): Int = a.compareTo(b)

Implicit параметры → Использование clauses

Контекстные параметры (или implicit параметры) помогают избежать написания повторяющихся параметров по цепочке вызовов. В Scala 3 вы используете implicit параметры через ключевое слово using. Например, из приведенных выше примеров можно определить функцию min , которая работает с ними.

def min[T](a: T, b: T)(using ord: Ord[T]): T =
  if ord.compare(a, b) < 0 then a else b

min(4, 2)min(1, 2)(using intOrd)
min("Foo", "Bar")

Когда вам нужно просто переадресовать параметры контекста, вам не нужно их называть.

def printMin[T](a: T, b: T)(using Ord[T]): Unit =
  println(min(a, b))

Implicit Импорт → Заданный Импорт

Бывают случаи, когда неправильный implicit импорт может стать причиной проблемы Кроме того, некоторые инструменты, такие как IDE и генераторы документации, не справляются с implicit импортом. Scala 3 предоставляет новый способ отличить заданный импорт от обычного.

object A:
  class TC
  given tc: TC = ???
  def f(using TC) = ???

object B:
  import A._
  import A.given
  ...

В приведенном выше примере нам пришлось импортировать заданные импорты отдельно, даже после импорта с помощью wildcad (_), потому что в Scala 3 заданные импорты работают не так, как обычные. Вы можете объединить оба импорта в один.

object C:
  import A.{using, _}

Вот некоторые спецификации, касающиеся заданных импортов по типам. Посмотрите на данную импортную документацию

Implicit Conversion → Заданная Conversion

Представим, что до Scala 3, если вы хотели определить Implicit Conversion, вам просто нужно было написать Implicit Conversion, которое получает экземпляр исходного типа и возвращает экземпляр целевого типа. Теперь нужно определить данный экземпляр класса scala.Conversion, который ведет себя как функция. Действительно, экземпляры scala. Conversion — это функции. Посмотрите на их определение.

abstract class Conversion[-T, +U] extends (T => U):
  def apply (x: T): U

Например, здесь можно посмотреть на преобразование от Int к Double и на более короткую версию:

given int2double: Conversion[Int, Double] with
def apply(a: Int): Double = a.toDouble

given Conversion[Int, Double] = _.toDouble

Основной причиной использования Заданной Conversion является наличие специального механизма преобразования стоимости без каких-либо сомнительных конфликтов с другими конструкторами языка. Согласно данной документации по преобразованию, все другие формы implicit преобразований будут постепенно ликвидированы.

Implicit классы → Методы расширения

Методы расширения являются более интуитивным и менее шаблонным способом, чем Implicit классы для добавления методов к уже определенным типам.

case class Image(width: Int, height: Int, data: Array[Byte])

extension (img: Image)
  def isSquare: Boolean = img.width == img.height

val image = Image(256, 256, readBytes("image.png"))

println(image.isSquare) // true

Методы расширения могут иметь параметры типа как в их определении, так и в их методах. Их определения также могут иметь несколько методов.

extension [T](list: List[T])
def second: T = list.tail.head
def heads: (T, T) = (list.head, second)

Как видите, методы расширения гораздо «чище», чем написание implicit классов. Обратите внимание, что в отличие от implicit классов, вам не нужно называть определения расширений.

Типы пересечения и соединения

Scala 3 предоставляет новые способы объединения типов, два из которых — Типы пересечения и соединения.

Типы пересечения

Типы пересечения — это типы, элементы которых принадлежат к обоим типам, составляющим его. Они определяются оператором & более двух типов. & являются коммутаторами: A & B производит один и тот же тип  B & A. Они также могут быть сцеплены, так как они тоже являются типами.

trait Printable[T]:
 def print(x: T): Unit

trait Cleanable:
 def clean(): Unit

trait Flushable:
 def flush(): Unit

def f(x: Printable[String] & Cleanable & Flushable) =
 x.print("working on...")
 x.flush()
 x.clean()

Вам может быть интересно узнать, как компилятор решает конфликты разделяемых элементов. Ответ состоит в том, что компилятор в этом не нуждается. Типы пересечений представляют собой требования к members типам (члены типов). Они работают практически так же, как и при формировании типов. В момент построения members необходимо просто убедиться, что все полученные members определены корректно.

trait A:
  def parent: Option[A]

trait B:
  def parent: Option[B]

class C extends A,B:
  def parent: Option[A & B] = None
  // or
  // def parent: Option[A] & Option[B] = Nil

def work(x: A & B) =
  val parent:[A & B] = x.parent
  // or
  // val parent: Option[A] & Option[B] = x.parent
  println(parent) // None

work(new C)

Заметьте, что в in class C нам нужно решить конфликты связанные с тем, что children member появляется и в A и в B. То есть тип C — это пересечение его типа в A и его типа в B, например, Option[A] & Option[B] могут быть упрощены в вид Option[A & B], так как Option (опция) является ковариантной.

Типы соединения

Тип соединения A | B  принимает все экземпляры типа A и все экземпляры типа B. Обратите внимание, что мы говорим об экземплярах, а не о members (членах), как это делают типы пересечения. Поэтому, если мы хотим получить доступ к его members, нам нужно сопоставить их по шаблону. 

def parseFloat(value: String | Int): Float = 
  value match 
    case str: String => str.toFloat
    case int: Int => int.floatValue

parseFloat("3.14") // 3.14
parseFloat(42) // 42.0

Типы соединения не выводятся автоматически. Если вы хотите, чтобы тип определения (val, var или def) был типом соединения, вам нужно сделать это явно, иначе компилятор выведет наименьший общий ancestor (предок).

val any = if (cond) 42 else "3.14" // Any
val union: String | Int = if (cond) 42 else "3.14" // String | Int

Почётные упоминания

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

Трейт параметры 

Scala 3 позволяет трейтам иметь параметры. Эти параметры оцениваются непосредственно перед инициализацией трейта. Параметры трейта являются заменой для ранних инициализаторов, которые были удалены из Scala 3.

Универсальные применяемые методы

Конструкторы Case class стали достаточно популярными, и многие разработчики пишут Case class просто для того, чтобы не писать new для создания объектов. Поэтому в Scala 3 больше не нужно писать new  для создания экземпляров классов. 

Типы Opaque 

Opaque-типы обеспечивают абстракцию типа без каких-либо перегрузок. Модифицируя определение типа с помощью Opaque, вы ограничиваете тот факт, что определение типа является просто псевдонимом для другого типа, где оно определено. Для клиентов своей области видимости Opaque-типы ведут себя идеально как тип, а не просто как псевдоним. Таким образом, например, вы не можете предполагать существование псевдонима для создания значений псевдонима и присвоения определений Opaque-типу.

Export clauses

Export clauses — это простой способ передачи members (членов) от одного типа к другому без какого-либо наследования. Откладывая export от членов-класса (включая трейты и объекты) в тело другого класса (также включая трейты и объекты), вы копируете members и делаете их доступными через экземпляры целевых классов.  

Редизайн метапрограммирования

В Scala 2 макросы оставались экспериментальной функцией. В связи с тем, что макросы сильно зависят от компилятора Scala 2, поэтому мигрировать на Scala 3 оказалось невозможным. В метапрограммировании Scala 3 появились новые конструкции, облегчающие его использование. Взгляните на обзор метапрограммирования Scala 3.

Ограничения и удаленные фитчи

Чтобы упростить язык и сделать его более безопасным, Scala 3 ограничивает количество опций и снимает некоторые функции с производства. Самые важные из них:

  • Ограничение проекций типов  (C#P)  только в отношении классов, т.е. абстрактные типы больше не поддерживают их;

  • Для использования надстрочной нотации, модификатор infix должен быть помечен на желаемых методах;

  • Мультиверсальное равенство - это оптический способ избегания неожиданных равнозначностей;

  • Implicit преобразования и приведенные выше импорты также являются видами ограничений;

  • Специальная обработка трейта DelayedInit больше не поддерживается;

  • Отброшен синтаксис процедуры (опускание типа возврата и = при определении функции);

  • Библиотеки XML все еще поддерживаются, но будут удалены в ближайшем будущем;

  • Автоматическое приложение, когда пустой список аргументов () неявно вставляется при вызове метода без аргументов и больше не поддерживается;

  • Символьные литералы больше не поддерживаются. 

Полный список выпавших функций доступен в официальной документации.

Нужно ли вам переходить на Scala 3?

Прежде всего, есть очень хорошо сделанная документация, посвященная исключительно переходу на Scala 3. Здесь мы просто поделимся некоторыми мыслями, которые вы можете учесть, когда начнете использовать Scala 3 в своих текущих проектах.

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

Какое время подходит для перехода на Scala 3?

Мы хотели бы порекомендовать вам перейти на Scala 3 прямо сейчас, но мы знаем, что есть факторы, выходящие за рамки возможностей даже большого энтузиазма. Если следующее звучит как отличный аргумент, чтобы убедить вашего руководителя, то не забывайте, что Scala 3 сохраняет как обратную, так и прямую совместимость с Scala 2.13 (за исключением макросов), не всю совместимость, но при каждой несовместимости есть решение для кросс-компиляции.

Что такое бинарная совместимость в Scala 3?

Scala 3 предлагает обратную двоичную совместимость с Scala 2. Это означает, что вы все еще можете полагаться на библиотеку Scala 2. Начиная с версии Scala 2.13.4, выпущенной в ноябре 2020 года, вы можете использовать библиотеки, написанные на Scala 3. Таким образом, в Scala 3 вы получаете двойную совместимость туда и обратно. 

Scala 3 поддерживает обратную и прямую совместимость с помощью уникального революционного механизма в экосистеме Scala. Scala 3 выводит файлы TASTy и поддерживает Pickle из версий Scala 2.x. Scala 2.13.4 поставляется с считывателями TASTy, поэтому поддерживает как все традиционные функции, так и новые, такие как Enums, Intersection types (типы соединения) и другие. Дополнительные сведения см. в руководстве по совместимости. 

Заключение

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

В целом, мне кажется, что Scala 3 — это отличная доработка Scala 2. Мы научились жить без многих вещей: для одних это были библиотеки, решавшие проблемы в той или иной степени с ограничениями, для других это было либо невозможно, либо выходило за пределы их зоны комфорта. Поэтому, как только Scala 3 получит широкое распространение, мы ожидаем увидеть более грамотно написанный код, в основном потому, что там это намного проще.


Узнать подробнее о курсе «Scala-разработчик».

Смотреть открытый вебинар «Эффекты в Scala».

Tags:
Hubs:
Total votes 13: ↑7 and ↓6+1
Comments7

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS