Третья из Неявных Сестер, но не конец истории.

Это третья часть из четырех частей. Если вы хотите прочитать обо всех трех формах неявного, вы можете начать здесь. Но если вы просто хотите узнать о неявных параметрах (и их аналогах в Scala 3, которые вводят аналоги given, using и summon), читайте дальше!

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

Краткое введение в каррирование

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

import java.text.NumberFormat
def makeString(n: Double, f: NumberFormat): String = f.format(n)
def reportError(n: Double, f: NumberFormat): String =
  makeString(n,f) + " isn't allowed here."
def printDouble(n: Double, f: NumberFormat): String = 
  makeString(n * 2,f)

Все эти функции принимают два входа (число и объект NumberFormat) и возвращают строку. В Scala есть интересная особенность: если у вас есть только один из двух входных данных, вы можете предоставить его и преобразовать функцию с двумя «дырами» в функцию только с одной «дырой». Мы делаем это, записывая входные данные в отдельные секции следующим образом:

def makeString(n: Double)(f: NumberFormat): String = f.format(n)

Если у меня есть оба входа, я могу передать их функции следующим образом:

scala> makeString(2.34)(NumberFormat.getPercentInstance)
val res0: String = 234%

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

scala> val twoThreeFour = makeString(2.34)(_)
val twoThreeFour: java.text.NumberFormat => String = $Lambda$1398/0x00000008011462d8@10d56aab
scala> twoThreeFour(NumberFormat.getCurrencyInstance)
val res1: String = $2.34

Этот пример не очень практичен, но он служит своей цели. Важно вот что: если вы видите функцию или метод с двумя или более наборами параметров, вы можете просто использовать их так же, как функцию с одним сводным списком параметров. Без суеты, без суеты, без забот. А теперь приступим к интересному!

Неявные параметры как волшебные инъекторы зависимостей

Учитывая мою причудливую числовую библиотеку в ее первоначальном виде, пользователи могут использовать эти три функции для записи чисел…

scala> val formatter = NumberFormat.getPercentInstance()
scala> reportError(1.2, formatter)
val res0: String = 120% isn't allowed here.
scala> printDouble(0.3, formatter)
val res1: String = 60%

… и все «хорошо», но не слишком ли это загромождено, всегда каждый раз передавая средство форматирования каждой функции? Это не способ ведения дел в Scala. Разработчики Scala любят, чтобы их код выглядел аккуратно, чисто и лаконично. Мы ненавидим шаблоны.

Что я действительно хочу сделать, так это объявить свой числовой формат один раз, в верхней части объявления блока кода или объекта, а затем не думать об этом снова.

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

def makeString(n: Double)(implicit f: NumberFormat): String = 
  f.format(n)
def reportError(n: Double)(implicit f: NumberFormat): String =
  makeString(n) + " isn't allowed here."
def printDouble(n: Double)(implicit f: NumberFormat): String =
  makeString(n * 2)

Теперь я могу использовать функции так же, как и раньше, но с двумя группами параметров, подобными этой:

scala> val formatter = NumberFormat.getPercentInstance()
scala> reportError(1.2)(formatter)
val res0: String = 120% isn't allowed here.
scala> printDouble(0.3)(formatter)
val res1: String = 60%

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

scala> implicit val formatter = NumberFormat.getPercentInstance()
scala> reportError(1.2)
val res0: String = 120% isn't allowed here.
scala> printDouble(0.3)
val res1: String = 60%

Используя неявное объявление, компилятор Scala позволяет мне навести порядок, чтобы мои глаза не отвлекались на средство форматирования. Это просто второстепенная, шаблонная функциональность, которая не так важна с точки зрения функциональности кода, который я пишу.

Теперь мне по-прежнему приходилось объявлять средство форматирования в фактических объявлениях методов, и мне приходилось каррировать объявления функций и добавлять ключевое слово implicit, чтобы мои объявления функций не выглядели короче, но если я пишу библиотеку, пользователи моей библиотеки могут сосредоточиться на важных вещах, а их код чист и элегантен. (Они по-прежнему должны не забывать где-то объявлять свою неявную переменную. Вы могли столкнуться с чем-то подобным при использовании чего-то вроде Scala Future, где вы должны были объявить неявную ExecutionContext. Если это так, теперь вы знаете, что это было такое.)

Использование альтернативного «неявного» синтаксиса

Чтобы быть полным, есть альтернативный способ получения неявных значений из эфира вместо того, чтобы записывать их как группу неявных параметров в вашем определении метода. Вместо того, чтобы писать что-то вроде этого:

def makeString(n: Double)(implicit fmt: NumberFormat): String = {
  fmt.format(n)
}

Вместо этого мы можем писать так:

def makeString(n: Double): String = {
  val fmt = implicitly[NumberFormat]
  fmt.format(n)
}

…или просто

def makeString(n: Double) = implicitly[NumberFormat].format(n)

Внутри компилятор делает то же самое: он обещает, что когда вы вызываете функцию makeString, где-то в контексте (в вашем коде или на который ссылается оператор import) будет неявное значение val типа NumberFormat, которое будет смог найти.

Выполнение этого с помощью Scala 3

Scala 3 очень похожа, только implicit заменена на using в неявном параметре, а implicit val заменена на given в неявном объявлении экземпляра.

def makeString(n: Double)(using f: NumberFormat): String =
  f.format(n)
def reportError(n: Double)(using f: NumberFormat): String =
  makeString(n) + " isn't allowed here."
def printDouble(n: Double)(using f: NumberFormat): String =
  makeString(n * 2)
given fmt: NumberFormat = NumberFormat.getPercentInstance
printDouble(2.32424)
val res0: String = 465%

или вместо старого синтаксиса implicitly мы можем использовать summon, который «вызывает» неявное значение из эфира следующим образом:

def makeString(n: Double): String =
  val fmt = summon[NumberFormat]
  fmt.format(n)

или просто

def makeString(n: Double) = summon[NumberFormat]format(n)

Мы покажем использование этого в заключительном разделе, где продемонстрируем объединение вещей с помощью классов типов.