Контекстные функции

Контекстные функции — это функции с параметрами контекста. Их типы являются типами контекстных функций. Вот пример подобного типа:

type Executable[T] = ExecutionContext ?=> T

Контекстные функции записываются с использованием ?=> знака "стрелка". Они применяются к синтезированным аргументам точно так же, как применяются методы с контекстными параметрами. Например:

given ec: ExecutionContext = ...

  def f(x: Int): ExecutionContext ?=> Int = ...

  // можно было бы написать следующим образом с псевдонимом типа выше
  // def f(x: Int): Executable[Int] = ...

  f(2)(using ec)   // явный аргумент
  f(2)             // выводимый аргумент

И наоборот, если ожидаемый тип выражения E является типом контекстной функции (T_1, ..., T_n) ?=> U и E не является литералом контекстной функции, E преобразуется в литерал контекстной функции путем перезаписи его в

(x_1: T1, ..., x_n: Tn) ?=> E

где имена x_1, ..., x_n произвольны. Это расширение выполняется перед проверкой типа выражения E, что означает, что x_1, ..., x_n доступны как данные в E.

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

Например, продолжая предыдущие определения,

def g(arg: Executable[Int]) = ...

  g(22)      // расширяется до g((ev: ExecutionContext) ?=> 22)

  g(f(2))    // расширяется до g((ev: ExecutionContext) ?=> f(2)(using ev))

  g((ctx: ExecutionContext) ?=> f(3))  // расширяется до g((ctx: ExecutionContext) ?=> f(3)(using ctx))
  g((ctx: ExecutionContext) ?=> f(3)(using ctx)) // осталось как есть

Пример: шаблон построителя

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

table {
  row {
    cell("top left")
    cell("top right")
  }
  row {
    cell("bottom left")
    cell("bottom right")
  }
}

Идея состоит в том, чтобы определить классы для Table и Row, которые позволяют добавлять элементы через add:

class Table:
  val rows = new ArrayBuffer[Row]
  def add(r: Row): Unit = rows += r
  override def toString = rows.mkString("Table(", ", ", ")")

class Row:
  val cells = new ArrayBuffer[Cell]
  def add(c: Cell): Unit = cells += c
  override def toString = cells.mkString("Row(", ", ", ")")

case class Cell(elem: String)

Затем можно определить методы конструктора table, row и cell с типами контекстных функций в качестве параметров.

def table(init: Table ?=> Unit) =
  given t: Table = Table()
  init
  t

def row(init: Row ?=> Unit)(using t: Table) =
   given r: Row = Row()
   init
   t.add(r)

def cell(str: String)(using r: Row) =
   r.add(new Cell(str))

При такой настройке приведенный выше код построения таблицы компилируется и расширяется до:

table { ($t: Table) ?=>

  row { ($r: Row) ?=>
    cell("top left")(using $r)
    cell("top right")(using $r)
  }(using $t)

  row { ($r: Row) ?=>
    cell("bottom left")(using $r)
    cell("bottom right")(using $r)
  }(using $t)
}

Пример пост-условия

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

object PostConditions:
  opaque type WrappedResult[T] = T

  def result[T](using r: WrappedResult[T]): T = r

  extension [T](x: T)
    def ensuring(condition: WrappedResult[T] ?=> Boolean): T =
      assert(condition(using x))
      x
end PostConditions
import PostConditions.{ensuring, result}

val s = List(1, 2, 3).sum.ensuring(result == 6)
// s: Int = 6

Пояснения: тип контекстной функции WrappedResult[T] ?=> Boolean используется в качестве типа условия ensuring. Таким образом, аргумент для ensuring такой, как (result == 6) будет иметь заданный тип WrappedResult[T] в области видимости для передачи методу result. WrappedResult является новым типом, чтобы убедиться, что мы не получаем нежелательных данных в области видимости (это хорошая практика во всех случаях, когда задействованы параметры контекста). Поскольку WrappedResult - это псевдоним opaque типа, его значения не нужно упаковывать, а поскольку ensuring добавляется как метод расширения, его аргумент также не нуждается в упаковывании. Следовательно, реализация ensuring близка по эффективности к наилучшему коду, который можно было бы написать вручную:

val s =
  val result = List(1, 2, 3).sum
  assert(result == 6)
  result

Ссылки: