Помимо синтаксического удобства, сочетание одноэлементных типов, типов, зависящих от пути и неявных значений, означает, что Scala имеет на удивление хорошую поддержку зависимой типизации, как я пытался продемонстрировать в shapeless .
Встроенная поддержка зависимых типов в Scala осуществляется через типы, зависящие от пути . Они позволяют типу зависеть от пути к селектору через граф объекта (т. Е. Значения) следующим образом:
scala> class Foo { class Bar }
defined class Foo
scala> val foo1 = new Foo
foo1: Foo = Foo@24bc0658
scala> val foo2 = new Foo
foo2: Foo = Foo@6f7f757
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, в качестве встроенных функций. Я думаю, что это в некоторой степени отражает зацикленность на форме над основами, тем не менее, я постараюсь показать, что Scala намного ближе к этим другим языкам, чем это обычно считается.
Несмотря на терминологию, типы зависимых сумм (также известные как типы сигма) - это просто пара значений, где тип второго значения зависит от первого значения. Это можно напрямую представить в 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} = $anon$1@e3fabd8
и на самом деле это важная часть кодирования типов зависимых методов, которая необходима для выхода из «Bakery of Doom» в Scala до 2.10 (или ранее с помощью экспериментальной опции компилятора Scala -Ydependent-method types).
Зависимые типы продуктов (также известные как 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} = $anon$1@60681a11
scala> implicit val barString = new Pi[Bar.type] { type U = String }
barString: java.lang.Object with Pi[Bar.type]{type U = String} = $anon$1@187602ae
А теперь вот наша функция использования Pi-типа в действии,
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. Вместо этого мы будем напрямую использовать типы, зависящие от пути, одиночные типы и имплициты. Вы можете найти множество примеров того, как это проявляется в бесформенных: типы с размерами , расширяемые записи , исчерпывающие списки HList , отбросьте свой шаблон , общие застежки-молнии и т. Д. И т. Д.
Единственное оставшееся возражение, которое я вижу, заключается в том, что в приведенном выше кодировании типов Pi мы требуем, чтобы одноэлементные типы зависимых значений были выражены. К сожалению, в Scala это возможно только для значений ссылочных типов, но не для значений не ссылочных типов (особенно, например, Int). Это позор, но не внутренняя трудность: тип проверки Scala представляет типы одноэлементные из не-эталонных значений внутри, и там было несколько из экспериментов сделать их непосредственно представимы. На практике мы можем обойти проблему с помощью довольно стандартного кодирования натуральных чисел на уровне типов .
В любом случае, я не думаю, что это небольшое ограничение домена можно использовать в качестве возражения против статуса Scala как языка с зависимой типизацией. Если это так, то то же самое можно сказать и о зависимом ML (который допускает зависимости только от значений натуральных чисел), что было бы странным выводом.