Ответ 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 (который допускает только зависимости от натуральных чисел), что было бы причудливым заключением.