Это моя вторая статья с обзором изменений в 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
и др.), или как реализовать концепции из теории категорий — монаду и моноид.
lgorSL
кстати, если метод один, можно слово extension не использовать.
nehaev Автор
В
M3
это не компилируется.lgorSL
Действительно, в более новых версиях убрали. Жалко, мне такой синтаксис кажется красивым обобщением над объявлением функций.