Dejando de lado la conveniencia sintáctica, la combinación de tipos singleton, tipos dependientes de la ruta y valores implícitos significa que Scala tiene un soporte sorprendentemente bueno para la escritura dependiente, como he intentado demostrar en informe .
El soporte intrínseco de Scala para los tipos dependientes es a través de los tipos dependientes de la ruta . Estos permiten que un tipo dependa de una ruta de selector a través de un gráfico de objeto (es decir, valor) como este,
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]
En mi opinión, lo anterior debería ser suficiente para responder a la pregunta "¿Es Scala un lenguaje de escritura dependiente?" en positivo: está claro que aquí tenemos tipos que se distinguen por los valores que son sus prefijos.
Sin embargo, a menudo se objeta que Scala no es un lenguaje de tipo "totalmente" dependiente porque no tiene tipos de producto y suma dependientes como los que se encuentran en Agda o Coq o Idris como intrínsecos. Creo que esto refleja una fijación en la forma sobre los fundamentos hasta cierto punto, sin embargo, intentaré mostrar que Scala está mucho más cerca de estos otros lenguajes de lo que normalmente se reconoce.
A pesar de la terminología, los tipos de suma dependientes (también conocidos como tipos Sigma) son simplemente un par de valores donde el tipo del segundo valor depende del primer valor. Esto se puede representar directamente en 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
y de hecho, esta es una parte crucial de la codificación de los tipos de métodos dependientes que se necesita para escapar de 'Bakery of Doom' en Scala antes de 2.10 (o antes a través de la opción del compilador Scala de tipos de métodos dependientes experimentales).
Los tipos de productos dependientes (también conocidos como tipos Pi) son esencialmente funciones de valores a tipos. Son clave para la representación de vectores de tamaño estático y los otros elementos secundarios del cartel para los lenguajes de programación de tipo dependiente. Podemos codificar tipos Pi en Scala usando una combinación de tipos dependientes de ruta, tipos singleton y parámetros implícitos. Primero definimos un rasgo que va a representar una función desde un valor de tipo T a un tipo U,
scala> trait Pi[T] { type U }
defined trait Pi
Podemos entonces definir un método polimórfico que utilice este tipo,
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]
(tenga en cuenta el uso del tipo dependiente de la ruta pi.U
en el tipo de resultado List[pi.U]
). Dado un valor de tipo T, esta función devolverá una lista (n vacía) de valores del tipo correspondiente a ese valor T en particular.
Ahora definamos algunos valores adecuados y testigos implícitos de las relaciones funcionales que queremos mantener,
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
Y ahora aquí está nuestra función de uso de tipo Pi en acción,
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]]
(tenga en cuenta que aquí usamos el <:<
operador testigo de subtipos de Scala en lugar de =:=
porque res2.type
y res3.type
son tipos singleton y, por lo tanto, más precisos que los tipos que estamos verificando en el RHS).
En la práctica, sin embargo, en Scala no comenzaríamos codificando los tipos Sigma y Pi y luego proceder desde allí como lo haríamos en Agda o Idris. En su lugar, usaríamos tipos dependientes de la ruta, tipos singleton e implícitos directamente. Puede encontrar numerosos ejemplos de cómo se desarrolla esto en informes sin forma: tipos de tamaño , registros extensibles , listas H completas , desecho de su plantilla , cremalleras genéricas , etc.
La única objeción restante que puedo ver es que en la codificación anterior de tipos Pi requerimos que los tipos singleton de los valores dependientes sean expresables. Desafortunadamente, en Scala esto solo es posible para valores de tipos de referencia y no para valores de tipos que no son de referencia (especialmente, por ejemplo, Int). Esto es una pena, pero no una dificultad intrínseca: el verificador de tipos de Scala representa los tipos singleton de valores no de referencia internamente, y ha habido un par de experimentos para hacerlos directamente expresables. En la práctica, podemos solucionar el problema con una codificación a nivel de tipo bastante estándar de los números naturales .
En cualquier caso, no creo que esta pequeña restricción de dominio pueda usarse como una objeción al estado de Scala como un lenguaje de tipo dependiente. Si es así, lo mismo podría decirse del ML dependiente (que solo permite dependencias de valores numéricos naturales), lo que sería una conclusión extraña.