Зависимые типы функций
Зависимый тип функции (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] = ... // implementation of DB
})
Необходимо вручную создать анонимный внутренний класс DB
, реализующий метод get
.
Для кода, основанного на создании множества различных экземпляров DB
, это очень утомительно.
DB
trait имеет только один абстрактный метод get
.
Было бы неплохо использовать в этом месте лямбда-синтаксис...
user { k =>
... // implementation of DB
}
На самом деле, в Scala 3 теперь это возможно! Можно определить DB
как зависимый тип функции:
type DB = (k: Key) => Option[k.Value]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// зависимый тип функции
Учитывая это определение DB
, можно использовать приведенный выше вызов user
.
Определение
Зависимый тип функции (dependent function type) — это тип функции, результат которой зависит от параметров функции. Например:
trait Entry { type Key; val key: Key }
def extractKey(e: Entry): e.Key = e.key // зависимый метод
val extractor: (e: Entry) => e.Key = extractKey // значение зависимой функции
// ^^^^^^^^^^^^^^^^^^^
// зависимый тип функции
Тип обычной функции A => B
представлен как экземпляр trait-а Function1
(т.е. Function1[A, B]
)
и аналогично для функций с большим количеством параметров.
Зависимые функции также представлены как экземпляры этих трейтов, но они получают дополнительное уточнение.
На самом деле, зависимый тип функции выше — это просто синтаксический сахар для
Function1[Entry, Entry#Key]:
def apply(e: Entry): e.Key
Практический пример: числовые выражения
Предположим, что необходимо определить модуль, который абстрагируется от внутреннего представления чисел. Это может быть полезно, например, для реализации библиотек для автоматического дифференцирования.
Начнем с определения модуля для чисел:
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) }
// ...
Дополнительные примеры
В приведенном ниже примере определяется trait C
и два зависимых типа функций DF
и IDF
,
а также выводятся результаты вызовов соответствующих функций:
trait C { type M; val m: M }
type DF = (x: C) => x.M
type IDF = (x: C) ?=> x.M
val c = new C { type M = Int; val m = 3 }
val depfun: DF = (x: C) => x.m
val idepfun: IDF = summon[C].m
val t = depfun(c)
// t: Int = 3
val u = idepfun(using c)
// u: Int = 3
В следующем примере тип зависимости f.Eff
относится к типу CanThrow
:
trait Effect
// Type X => Y
abstract class Fun[-X, +Y]:
type Eff <: Effect
def apply(x: X): Eff ?=> Y
class CanThrow extends Effect
class CanIO extends Effect
given ct: CanThrow = new CanThrow
given ci: CanIO = new CanIO
class I2S extends Fun[Int, String]:
type Eff = CanThrow
def apply(x: Int) = x.toString
class S2I extends Fun[String, Int]:
type Eff = CanIO
def apply(x: String) = x.length
// def map(f: A => B)(xs: List[A]): List[B]
def map[A, B](f: Fun[A, B])(xs: List[A]): f.Eff ?=> List[B] =
xs.map(f.apply)
// def mapFn[A, B]: (A => B) -> List[A] -> List[B]
def mapFn[A, B]: (f: Fun[A, B]) => List[A] => f.Eff ?=> List[B] =
f => xs => map(f)(xs)
// def compose(f: A => B)(g: B => C)(x: A): C
def compose[A, B, C](f: Fun[A, B])(g: Fun[B, C])(x: A): f.Eff ?=> g.Eff ?=> C =
g(f(x))
// def composeFn: (A => B) -> (B => C) -> A -> C
def composeFn[A, B, C]: (f: Fun[A, B]) => (g: Fun[B, C]) => A => f.Eff ?=> g.Eff ?=> C =
f => g => x => compose(f)(g)(x)
val i2s = new I2S
val s2i = new S2I
mapFn(i2s)(List(1, 2, 3)).mkString
// res0: String = "123"
composeFn(i2s)(s2i)(22)
// res1: Int = 2
Ссылки: