Любая причина, по которой scala явно не поддерживает зависимые типы?

Существуют типы, зависящие от пути, и я думаю, что можно выразить почти все функции таких языков, как Epigram или Agda, в Scala, но мне интересно, почему Scala не поддерживает это более явно, как это очень хорошо в других областях (скажем, DSL)? Все, что мне не хватает, как "не нужно"?

Ответ 1

Синтаксическое удобство в стороне, комбинация одноэлементных типов, зависимых от пути типов и неявных значений означает, что Scala имеет удивительно хорошую поддержку зависимого набора текста, поскольку я попытался продемонстрировать в shapeless.

Scala внутренняя поддержка зависимых типов осуществляется через зависимые от пути типы. Они позволяют типу зависеть от пути выбора по графику объекта (т.е. Value-), например,

scala> class Foo { class Bar }
defined class Foo

scala> val foo1 = new Foo
foo1: Foo = [email protected]

scala> val foo2 = new Foo
foo2: Foo = [email protected]

scala> implicitly[foo1.Bar =:= foo1.Bar] // OK: equal types
res0: =:=[foo1.Bar,foo1.Bar] = <function1>

scala> implicitly[foo1.Bar =:= foo2.Bar] // Not OK: unequal types
<console>:11: error: Cannot prove that foo1.Bar =:= foo2.Bar.
              implicitly[foo1.Bar =:= foo2.Bar]

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

Однако он часто возражал, что Scala не является полностью "зависимым" языком, потому что у него нет зависимых сумм и типов продуктов, как это было найдено в Agda или Coq или Idris как intrinsics. Я думаю, что это в какой-то мере отражает фиксацию формы по фундаментальным принципам, тем не менее, я попытаюсь показать, что Scala намного ближе к этим другим языкам, чем обычно подтверждается.

Несмотря на терминологию, зависимые типы сумм (также известные как типы Sigma) представляют собой просто пару значений, где тип второго значения зависит от первого значения. Это прямо представимо в Scala,

scala> trait Sigma {
     |   val foo: Foo
     |   val bar: foo.Bar
     | }
defined trait Sigma

scala> val sigma = new Sigma {
     |   val foo = foo1
     |   val bar = new foo.Bar
     | }
sigma: java.lang.Object with Sigma{val bar: this.foo.Bar} = [email protected]

и на самом деле, это важная часть кодирования зависимых типов методов, которая необходима для выхода из "Хлебопека Doom" в Scala до до 2.10 (или ранее с помощью опции экспериментального-зависимого метода Scala вариант компилятора).

Типы зависимых продуктов (также называемые типами Pi) по существу являются функциями от значений к типам. Они являются ключевыми для представления векторов статического размера и других детей-плакатов для зависимых языков программирования. Мы можем кодировать типы Pi в Scala с использованием комбинации зависимых от пути типов, одноэлементных типов и неявных параметров. Сначала мы определяем признак, который будет представлять функцию от значения типа T до типа U,

scala> trait Pi[T] { type U }
defined trait Pi

Мы можем определить полиморфный метод, который использует этот тип,

scala> def depList[T](t: T)(implicit pi: Pi[T]): List[pi.U] = Nil
depList: [T](t: T)(implicit pi: Pi[T])List[pi.U]

(обратите внимание на использование зависимого от пути типа pi.U в типе результата List[pi.U]). Учитывая значение типа T, эта функция возвращает (n пустой) список значений типа, соответствующего этому конкретному значению T.

Теперь давайте определим некоторые подходящие значения и неявные свидетельства для функциональных отношений, которые мы хотим сохранить,

scala> object Foo
defined module Foo

scala> object Bar
defined module Bar

scala> implicit val fooInt = new Pi[Foo.type] { type U = Int }
fooInt: java.lang.Object with Pi[Foo.type]{type U = Int} = [email protected]

scala> implicit val barString = new Pi[Bar.type] { type U = String }
barString: java.lang.Object with Pi[Bar.type]{type U = String} = [email protected]

И вот теперь наша функция Pi-type-use в действии,

scala> depList(Foo)
res2: List[fooInt.U] = List()

scala> depList(Bar)
res3: List[barString.U] = List()

scala> implicitly[res2.type <:< List[Int]]
res4: <:<[res2.type,List[Int]] = <function1>

scala> implicitly[res2.type <:< List[String]]
<console>:19: error: Cannot prove that res2.type <:< List[String].
              implicitly[res2.type <:< List[String]]
                    ^

scala> implicitly[res3.type <:< List[String]]
res6: <:<[res3.type,List[String]] = <function1>

scala> implicitly[res3.type <:< List[Int]]
<console>:19: error: Cannot prove that res3.type <:< List[Int].
              implicitly[res3.type <:< List[Int]]

(обратите внимание, что здесь мы используем оператор Scala <:< подтип-свидетель, а не =:=, потому что res2.type и res3.type являются одноточечными типами и, следовательно, более точными, чем типы, которые мы проверяем на RHS).

На практике, однако, в Scala мы не будем начинать с кодирования типов Sigma и Pi, а затем исходя из этого, как и в Agda или Idris. Вместо этого мы будем использовать напрямую зависящие от типа типы, singleton-типы и implicits. Вы можете найти многочисленные примеры того, как это происходит в бесформенном виде: размерные типы, расширяемые записи, всеобъемлющие HLists, отмените ваш шаблон, общие молнии и т.д. и т.д.

Единственное оставшееся возражение, которое я вижу, заключается в том, что в приведенной выше кодировке типов Pi мы требуем, чтобы однотипные типы зависимых значений были выражены. К сожалению, в Scala это возможно только для значений ссылочных типов, а не для значений типов без ссылки (например, Int). Это позор, но не внутренняя сложность: Scala type checker представляет собой одиночные типы значений без ссылки внутри, а также пара эксперименты, чтобы сделать их прямо выраженными. На практике мы можем решить эту проблему с помощью довольно стандартного кодирования на натуральном уровне.

В любом случае, я не думаю, что это небольшое ограничение домена может быть использовано как возражение на статус Scala как язык с навязчивым типом. Если это так, то то же самое можно сказать и для зависимого ML (который допускает только зависимости от натуральных чисел), что было бы причудливым заключением.

Ответ 2

Я бы предположил, что это потому, что (как я знаю по опыту, используя зависимые типы в помощнике Coq proof, который полностью поддерживает их, но все же не очень удобен) зависимые типы - это очень продвинутая функция языка программирования, которая действительно трудно получить право - и может вызвать экспоненциальное разложение в сложности на практике. Они все еще являются темой исследований в области компьютерных наук.

Ответ 3

Я считаю, что Scala зависимые от пути типы могут представлять только Σ-типы, но не Π-типы. Это:

trait Pi[T] { type U }

не является точно П-типом. По определению, П-тип или зависимый продукт является функцией, результат которой зависит от значения аргумента, представляющего универсальный квантификатор, т.е. ∀x: A, B (x). Однако в вышеприведенном случае это зависит только от типа T, но не от некоторого значения этого типа. Сам признак Pi представляет собой Σ-тип, квантор существования, т.е. ∃x: A, B (x). Самооценка объекта в этом случае действует как количественная переменная. Однако, когда он передается как неявный параметр, он сводится к обычной функции типа, поскольку он разрешен по типу. Кодировка для зависимого продукта в Scala может выглядеть следующим образом:

trait Sigma[T] {
  val x: T
  type U //can depend on x
}

// (t: T) => (∃ mapping(x, U), x == t) => (u: U); sadly, refinement won't compile
def pi[T](t: T)(implicit mapping: Sigma[T] { val x = t }): mapping.U 

Отсутствующий фрагмент здесь - это способность статически связать поле x с ожидаемым значением t, эффективно формируя уравнение, представляющее свойство всех значений, обитающих в типе T. Вместе с нашими Σ-типами, используемыми для выражения существования объекта с заданным свойство, формируется логика, в которой наше уравнение является доказанной теоремой.

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