Это моя вторая статья с обзором изменений в Scala 3. Первая статья была про новый бесскобочный синтаксис.


Одна из наиболее известных фич языка Scala — имплиситы (от англ. implicit — неявный — прим. перев.), механизм, который использовался для нескольких разных целей, например: эмуляция extension-методов (обсудим в этой статье), неявная передача параметров при вызове метода, наложение ограничений на возможный тип и др. Все это — способы абстрагирования контекста.


Для освоения Scala требовалось в том числе научиться грамотно применять механизм имплиситов и связанные с ним идиомы. И это был серьезный вызов для новичков.


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


Scala 3 будет побуждать вас начать этот переход, при этом все старые способы использования имплиситов (с небольшими изменениями для большей безопасности) по-прежнему будут работать.


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


Синтаксис примеров актуален на момент Scala 3.0.0-M3.

Extension-методы


Один из способов создания кортежа из двух элементов в Scala — использовать a -> b, альтернативу привычному всем (a, b). В Scala 2 это реализовано с помощью неявного преобразования из типа переменной a в ArrowAssoc, где определен метод ->:


implicit final class ArrowAssoc[A](private val self: A) extends AnyVal {
  @inline def -> [B](y: B): (A, B) = (self, y)
  @deprecated("Use `->` instead...", "2.13.0")
  def >[B](y: B): (A, B) = ->(y)
}

Обратите внимание, что юникодовская стрелочка > помечена как deprecated. Не буду объяснять другие детали, типа @inline. (Ну ладно, эта аннотация говорит компилятору пытаться инлайнить этот код, избегая оверхеда на вызов метода...)


Это довольно типично для Scala 2: если хочется чтобы метод казался частью типа, нужно сделать неявное преобразование к типу-обертке, который предоставляет этот метод.


Другими словами, Scala 2 использует универсальный механизм имплиситов, чтобы достичь конкретной цели — появления extension-метода. Именно так в других языках (например, в C#) называется способ добавления к типу метода, который объявлен вне этого типа.


В Scala 3 extension-методы становятся сущностями первого класса. Вот как теперь можно переписать ArrowAssoc, используя ~> в качестве имени метода (поскольку настоящий ArrowAssoc все еще существует в Scala 3):


// From https://github.com/deanwampler/programming-scala-book-code-examples/
import scala.annotation.targetName

extension [A, B] (a: A)
  @targetName("arrow2") def ~>(b: B): (A, B) = (a, b) 

Сначала идет ключевое слово extension, после него типы-параметры (в нашем случае — [A, B]). A — это тип, который мы расширяем, значение a позволяет сослаться на экземпляр этого типа, для которого был вызван наш extension-метод (аналог this). Обратите внимание, что я использую новый бесскобочный синтаксис, который мы обсуждали в предыдущей статье. После ключевого слова extension можно указать сколько угодно методов.


Еще одно нововведение в Scala 3 — аннотация @targetName. С ее помощью можно определить буквенно-цифровое имя для методов, выполняющих в Scala роль операторов. Это имя нельзя будет использовать из Scala-кода (нельзя написать a.arrow2(b)), зато можно использовать из Java-кода, чтобы вызвать такой метод. Использовать @targetName теперь рекомендуется для всех "операторных" методов.


Неявные преобразования


С появлением extension-методов вам гораздо реже будет нужна возможность конвертации из одного типа в другой, однако иногда такая возможность также может пригодиться. Например, у вас есть финансовое приложение с case-классами для суммы в валюте, процента налогов и зарплаты. Вы хотите для удобства указывать значения этих величин как литерал типа double с последующим неявным преобразованием в типы из предметной области. Вот как это будет выглядеть в интерпретаторе Scala 3:


scala> import scala.language.implicitConversions
scala> case class Dollars(amount: Double):
     |   override def toString = f"$$$amount%.2f"
     | case class Percentage(amount: Double):
     |   override def toString = f"${(amount*100.0)}%.2f%%" 
     | case class Salary(gross: Dollars, taxes: Percentage):
     |   def net: Dollars = Dollars(gross.amount * (1.0 - taxes.amount))
// defined case class Dollars
// defined case class Percentage
// defined case class Salary

scala> given Conversion[Double,Dollars] = d => Dollars(d)
def given_Conversion_Double_Dollars: Conversion[Double, Dollars]

scala> given d2P: Conversion[Double,Percentage] = d => Percentage(d) 
def d2P: Conversion[Double, Percentage]

scala> val salary = Salary(100_000.0, 0.20)
scala> println(s"salary: $salary. Net pay: ${salary.net}")
salary: Salary($100000.00,20.00%). Net pay: $80000.00

Сначала мы объявляем, что будем использовать неявные преобразования. Для этого надо импортировать implicitConversions. Затем объявляем три case-класса, которые нужны в нашей предметной области.


Далее показан новый способ объявления неявных преобразований. Ключевое слово given заменяет старое implicit def. Смысл остался тот же, но есть небольшие отличия. Для каждого объявления генерируется специальный метод. Если неявное преобразование анонимное, название этого метода также будет сгенерировано автоматически (обратите внимание на префикс given_Conversion в имени метода для первого преобразования).


Новый абстрактный класс Conversion содержит метод apply, в который компилятор подставит тело анонимной функции, которая идет после =. Если необходимо, метод apply можно переопределить явно:


given Conversion[Double,Dollars] with
  def apply(d: Double): Dollars = Dollars(d)

Ключевое слово with знакомо нам по подмешиванию трейтов в Scala 2. Здесь его можно интерпретировать как подмешивание анонимного трейта, который переопределяет реализацию apply в классе Conversion.


Возвращаясь к предыдущему примеру, хотелось бы отметить еще одну новую возможность (на самом деле она появилась еще в 2.13 — прим. перев.): можно вставлять подчеркивания _ в длинные числовые литералы для улучшения читаемости. Вы могли такое видеть например в Python (или в Java 7+ — прим. перев.).


Scala 3 по-прежнему поддерживает implicit-методы из Scala 2. Например, конвертацию из Double в Dollars можно было бы записать так:


implicit def toDollars(d: Double): Dollars = Dollars(d)

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


Что дальше?


В следующей статье мы рассмотрим новый синтаксис для тайпклассов, который сочетает given и extension-методы. Для затравки, подумайте, как можно было бы добавить метод toJson к типам из нашей предметной области (Dollars и др.), или как реализовать концепции из теории категорий — монаду и моноид.