Предложения using

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

Например, с экземплярами given intOrd и listOrd функция max, которая работает для любых аргументов с возможностью упорядочивания, может быть определена следующим образом:

def max[T](x: T, y: T)(using ord: Ord[T]): T =
  if ord.compare(x, y) < 0 then y else x

Здесь параметр контекста ord вводится с предложением using. Начав секцию параметров с ключевого слова using, мы сообщаем компилятору Scala, что на месте вызова он должен автоматически найти аргумент с правильным типом. Таким образом, компилятор Scala выполняет вывод терминов (term inference). Функцию max можно применить следующим образом:

max(2, 3)(using intOrd)
// res0: Int = 3

Часть (using intOrd) передает intOrd как аргумент для параметра ord. Но смысл параметров контекста в том, что этот аргумент также можно опустить (и это обычно так и есть). Таким образом, следующие выражения валидны:

max(2, 3)
// res1: Int = 3
max(List(1, 2, 3), Nil)
// res2: List[Int] = List(1, 2, 3)

В вызове max(2, 3) компилятор Scala видит, что в области действия есть терм типа Ord[Int], и автоматически предоставит его в max.

Анонимные параметры контекста

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

//                  не нужно придумывать имя параметра
//                          vvvvvvvvvvvv
def maximum[T](xs: List[T])(using Ord[T]): T =
  xs.reduceLeft(max)

maximum принимает контекстный параметр типа Ord[T] только для того, чтобы передать его в качестве предполагаемого аргумента в max. Имя параметра опущено.

maximum(List(1, 2, 3))
// res3: Int = 3

Как правило, параметры контекста могут быть определены либо как полный список параметров, (p_1: T_1, ..., p_n: T_n) либо как последовательность типов T_1, ..., T_n. Параметры Vararg не поддерживаются в using предложениях.

Явное предоставление контекстных аргументов

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

maximum(List(1, 2, 3))(using intOrd)
// res4: Int = 3

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

Параметры контекста класса

Если параметр контекста класса становится элементом путем добавления модификатора val или var, то этот член доступен как экземпляр given.

Сравните следующие примеры, в которых попытка указать явный элемент given приводит к двусмысленности:

class GivenIntBox(using val givenInt: Int):
  def n = summon[Int]

class GivenIntBox2(using givenInt: Int):
  given Int = givenInt
  //def n = summon[Int] // неопределенность

Элемент given можно импортировать, как описано в разделе об импорте givens:

val b = GivenIntBox(using 23)
import b.given
summon[Int]  // 23

import b.*
//givenInt // Not found

Вывод сложных аргументов

Вот два других метода, которые используют контекстный параметр типа Ord[T]:

def descending[T](using asc: Ord[T]): Ord[T] = new Ord[T]:
  def compare(x: T, y: T) = asc.compare(y, x)

def minimum[T](xs: List[T])(using Ord[T]) =
  maximum(xs)(using descending)

Тело метода minimum передает descending как явный аргумент в maximum(xs). С этой настройкой все следующие вызовы нормализуются так:

val xs = List(List(1,2,3), Nil)
// xs: List[List[Int]] = List(List(1, 2, 3), List())
minimum(xs)
// res5: List[Int] = List()
maximum(xs)(using descending)
// res6: List[Int] = List()
maximum(xs)(using descending(using listOrd))
// res7: List[Int] = List()
maximum(xs)(using descending(using listOrd(using intOrd)))
// res8: List[Int] = List()

Несколько using

В определении может быть несколько using предложений, и using предложения можно свободно смешивать с обычными предложениями параметров. Пример:

def f(u: Universe)(using ctx: u.Context)(using s: ctx.Symbol, k: ctx.Kind) = ...

В коде несколько using предложений сопоставляются слева направо. Пример:

object global extends Universe { type Context = ... }
given ctx : global.Context with { type Symbol = ...; type Kind = ... }
given sym : ctx.Symbol
given kind: ctx.Kind

Тогда все следующие вызовы действительны (и нормализуются до последнего)

f(global)
f(global)(using ctx)
f(global)(using ctx)(using sym, kind)

Но f(global)(using sym, kind) выдало бы ошибку типа.

Вызов экземпляров

Метод summon в Predef возвращает given определенного типа. Например, вот как можно вызвать given экземпляр для Ord[List[Int]]:

summon[Ord[List[Int]]]  // сводится к listOrd(using intOrd)

Метод summon определяется как (нерасширяющая) функция идентификации по параметру контекста.

def summon[T](using x: T): x.type = x

Ссылки: