Scala 3 — Book

Зависимые типы функций

Language
Эта страница документа относится к Scala 3 и может охватывать новые концепции, недоступные в Scala 2. Если не указано явно, все примеры кода на этой странице предполагают, что вы используете Scala 3.

Зависимый тип функции (dependent function type) описывает типы функций, где тип результата может зависеть от значений параметров функции. Концепция зависимых типов и типов зависимых функций является более продвинутой, и обычно с ней сталкиваются только при разработке собственных библиотек или использовании расширенных библиотек.

Зависимые типы методов

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

trait Key { type Value }

trait DB {
  def get(k: Key): Option[k.Value] // зависимый метод
}

Получив ключ, метод get предоставляет доступ к карте и потенциально возвращает сохраненное значение типа k.Value. Мы можем прочитать этот path-dependent type как: “в зависимости от конкретного типа аргумента k возвращается соответствующее значение”.

Например, у нас могут быть следующие ключи:

object Name extends Key { type Value = String }
object Age extends Key { type Value = Int }

Вызовы метода get теперь будут возвращать такие типы:

val db: DB = ...
val res1: Option[String] = db.get(Name)
val res2: Option[Int] = db.get(Age)

Вызов метода db.get(Name) возвращает значение типа Option[String], а вызов db.get(Age) возвращает значение типа Option[Int]. Тип возвращаемого значения зависит от конкретного типа аргумента, переданного для get — отсюда и название dependent type.

Зависимые типы функций

Как видно выше, в Scala 2 уже была поддержка зависимых типов методов. Однако создание значений типа DB довольно громоздко:

// создание пользователя DB
def user(db: DB): Unit =
  db.get(Name) ... db.get(Age)

// создание экземпляра DB и передача его `user`
user(new DB {
  def get(k: Key): Option[k.Value] = ... // реализация DB
})

Необходимо вручную создать анонимный внутренний класс DB, реализующий метод get. Для кода, основанного на создании множества различных экземпляров DB, это очень утомительно.

Трейт DB имеет только один абстрактный метод get. Было бы неплохо использовать в этом месте лямбда-синтаксис?

user { k =>
  ... // реализация DB
}

На самом деле, в Scala 3 теперь это возможно! Можно определить DB как зависимый тип функции:

type DB = (k: Key) => Option[k.Value]
//        ^^^^^^^^^^^^^^^^^^^^^^^^^^^
//        зависимый тип функции

Учитывая это определение DB, можно использовать приведенный выше вызов user.

Подробнее о внутреннем устройстве зависимых типов функций можно прочитать в справочной документации.

Практический пример: числовые выражения

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

Начнем с определения модуля для чисел:

trait Nums:
  // тип Num оставлен абстрактным
  type Num

  // некоторые операции над числами
  def lit(d: Double): Num
  def add(l: Num, r: Num): Num
  def mul(l: Num, r: Num): Num

Здесь опускается конкретная реализация Nums, но в качестве упражнения можно реализовать Nums, назначив тип Num = Double и реализуя соответствующие методы.

Программа, использующая числовую абстракцию, теперь имеет следующий тип:

type Prog = (n: Nums) => n.Num => n.Num

val ex: Prog = nums => x => nums.add(nums.lit(0.8), x)

Тип функции, которая вычисляет производную, наподобие ex:

def derivative(input: Prog): Double

Учитывая удобство зависимых типов функций, вызов этой функции в разных программах прост:

derivative { nums => x => x }
derivative { nums => x => nums.add(nums.lit(0.8), x) }
// ...

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

derivative(new Prog {
  def apply(nums: Nums)(x: nums.Num): nums.Num = x
})
derivative(new Prog {
  def apply(nums: Nums)(x: nums.Num): nums.Num = nums.add(nums.lit(0.8), x)
})
// ...

Комбинация с контекстными функциями

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

trait NumsDSL extends Nums:
  extension (x: Num)
    def +(y: Num) = add(x, y)
    def *(y: Num) = mul(x, y)

def const(d: Double)(using n: Nums): n.Num = n.lit(d)

type Prog = (n: NumsDSL) ?=> n.Num => n.Num
//                       ^^^
//     prog теперь - контекстная функция,
//     которая неявно предполагает NumsDSL в контексте вызова

def derivative(input: Prog): Double = ...

// теперь нам не нужно упоминать Nums в приведенных ниже примерах
derivative { x => const(1.0) + x }
derivative { x => x * x + const(2.0) }
// ...

Contributors to this page: