Scala 3: избавление от implicit. Extension-методы и неявные преобразования

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

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!


Это моя вторая статья с обзором изменений в 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 и др.), или как реализовать концепции из теории категорий — монаду и моноид.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Как вам новые extension-методы?

  • 100,0%Гораздо лучше, чем раньше1
  • 0,0%Лучше бы оставили implicit0

Нужны ли extension-методы в современном ООП-языке?

  • 50,0%Конечно! В моем основном языке они есть1
  • 50,0%Да, но в моем основном языке их нет1
  • 0,0%Не нужны, в ООП такие проблемы решаются наследованием, адаптерами или игнорированием0
Источник: https://habr.com/ru/post/537340/


Интересные статьи

Интересные статьи

Есть несколько способов добавить водяной знак в Битрикс. Рассмотрим два способа.
Есть статьи о недостатках Битрикса, которые написаны программистами. Недостатки, описанные в них рядовому пользователю безразличны, ведь он не собирается ничего программировать.
В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения. В целом, я не согласен с этой точкой зрения. На мой взгляд, я...
На сегодняшний день у сервиса «Битрикс24» нет сотен гигабит трафика, нет огромного парка серверов (хотя и существующих, конечно, немало). Но для многих клиентов он является основным инструментом ...
Реализация ORM в ядре D7 — очередная интересная, перспективная, но как обычно плохо документированная разработка от 1с-Битрикс :) Призвана она абстрагировать разработчика от механики работы с табл...