Conveniência sintática à parte, a combinação de tipos singleton, tipos dependentes de caminho e valores implícitos significa que Scala tem um suporte surpreendentemente bom para tipagem dependente, como tentei demonstrar no sem forma .
O suporte intrínseco do Scala para tipos dependentes é por meio de tipos dependentes de caminho . Isso permite que um tipo dependa de um caminho de seletor através de um gráfico de objeto (ou seja, valor), assim,
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]
Em minha opinião, o que foi dito acima deve ser suficiente para responder à pergunta "Scala é uma linguagem de tipo dependente?" no positivo: é claro que aqui temos tipos que se distinguem pelos valores que são seus prefixos.
No entanto, muitas vezes é objetado que Scala não é uma linguagem de tipo "totalmente" dependente porque não tem soma dependente e tipos de produto como encontrados em Agda ou Coq ou Idris como intrínsecos. Acho que isso reflete uma fixação na forma sobre os fundamentos até certo ponto, no entanto, tentarei mostrar que o Scala está muito mais próximo dessas outras linguagens do que normalmente se reconhece.
Apesar da terminologia, os tipos de soma dependentes (também conhecidos como tipos Sigma) são simplesmente um par de valores em que o tipo do segundo valor depende do primeiro valor. Isso é diretamente representável no 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
e, de fato, essa é uma parte crucial da codificação de tipos de métodos dependentes que são necessários para escapar da 'Padaria da Destruição' em Scala antes de 2.10 (ou anterior por meio da opção de compilador Scala experimental -Ydependent-method types).
Os tipos de produtos dependentes (também conhecidos como tipos Pi) são essencialmente funções de valores para tipos. Eles são a chave para a representação de vetores de tamanho estático e os outros filhos do pôster para linguagens de programação com tipos dependentes. Podemos codificar tipos Pi no Scala usando uma combinação de tipos dependentes de caminho, tipos singleton e parâmetros implícitos. Primeiro, definimos um traço que vai representar uma função de um valor do tipo T para um tipo U,
scala> trait Pi[T] { type U }
defined trait Pi
Podemos então definir um método polimórfico que usa 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]
(observe o uso do tipo dependente do caminho pi.U
no tipo de resultado List[pi.U]
). Dado um valor do tipo T, esta função retornará uma lista (n vazia) de valores do tipo correspondente a esse valor T específico.
Agora vamos definir alguns valores adequados e testemunhas implícitas para as relações funcionais que queremos manter,
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
E agora aqui está nossa função de uso do tipo Pi em ação,
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]]
(observe que aqui usamos o <:<
operador de testemunha de subtipo de Scala em vez de =:=
porque res2.type
e res3.type
são tipos singleton e, portanto, mais precisos do que os tipos que verificamos no RHS).
Na prática, entretanto, em Scala não começaríamos codificando os tipos Sigma e Pi e continuando daí como faríamos em Agda ou Idris. Em vez disso, usaríamos tipos dependentes de caminho, tipos singleton e implícitos diretamente. Você pode encontrar vários exemplos de como isso funciona sem forma: tipos de tamanhos , registros extensíveis , HLists abrangentes , retire seu clichê , Zíperes genéricos etc. etc.
A única objeção remanescente que posso ver é que na codificação acima dos tipos Pi exigimos que os tipos singleton dos valores dependentes sejam expressos. Infelizmente, em Scala, isso só é possível para valores de tipos de referência e não para valores de tipos sem referência (esp. Por exemplo, Int). Isto é uma vergonha, mas não uma dificuldade intrínseca: verificador de tipos de Scala representa os tipos únicos de valores não-referência internamente, e tem havido um par de experimentos em fazê-los diretamente expresso. Na prática, podemos contornar o problema com uma codificação de nível de tipo razoavelmente padrão dos números naturais .
Em qualquer caso, não acho que esta pequena restrição de domínio possa ser usada como uma objeção ao status do Scala como uma linguagem de tipo dependente. Se for, então o mesmo poderia ser dito para Dependent ML (que só permite dependências em valores de números naturais), o que seria uma conclusão bizarra.