A parte la comodità sintattica, la combinazione di tipi singleton, tipi dipendenti dal percorso e valori impliciti significa che Scala ha un supporto sorprendentemente buono per la tipizzazione dipendente, come ho cercato di dimostrare in informe .
Il supporto intrinseco di Scala per i tipi dipendenti è tramite i tipi dipendenti dal percorso . Questi consentono a un tipo di dipendere da un percorso del selettore attraverso un oggetto- (cioè valore-) grafico in questo modo,
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]
A mio avviso, quanto sopra dovrebbe essere sufficiente per rispondere alla domanda "Scala è un linguaggio tipizzato in modo dipendente?" in positivo: è chiaro che qui abbiamo dei tipi che si distinguono per i valori che sono i loro prefissi.
Tuttavia, spesso si obietta che Scala non è un linguaggio di tipo "completamente" dipendente perché non ha somma dipendente e tipi di prodotto come si trovano in Agda o Coq o Idris come intrinseci. Penso che questo rifletta in una certa misura una fissazione sulla forma rispetto ai fondamentali, tuttavia, cercherò di mostrare che Scala è molto più vicino a questi altri linguaggi di quanto sia tipicamente riconosciuto.
Nonostante la terminologia, i tipi di somma dipendente (noti anche come tipi Sigma) sono semplicemente una coppia di valori in cui il tipo del secondo valore dipende dal primo valore. Questo è direttamente rappresentabile in 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 in effetti, questa è una parte cruciale della codifica dei tipi di metodo dipendenti che è necessaria per uscire dal "Bakery of Doom" in Scala prima della 2.10 (o precedente tramite l'opzione sperimentale del compilatore Scala dei tipi di metodo -Ydependent).
I tipi di prodotto dipendenti (noti anche come tipi Pi) sono essenzialmente funzioni dai valori ai tipi. Sono fondamentali per la rappresentazione di vettori di dimensioni statiche e gli altri figli poster per linguaggi di programmazione tipizzati in modo dipendente. Possiamo codificare i tipi Pi in Scala utilizzando una combinazione di tipi dipendenti dal percorso, tipi singleton e parametri impliciti. Per prima cosa definiamo un tratto che rappresenterà una funzione da un valore di tipo T a un tipo U,
scala> trait Pi[T] { type U }
defined trait Pi
Possiamo quindi definire un metodo polimorfico che utilizza questo 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]
(notare l'uso del tipo dipendente dal percorso pi.U
nel tipo di risultato List[pi.U]
). Dato un valore di tipo T, questa funzione restituirà un elenco (n vuoto) di valori del tipo corrispondente a quel particolare valore T.
Definiamo ora alcuni valori appropriati e testimoni impliciti per le relazioni funzionali che vogliamo mantenere,
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 ora ecco la nostra funzione che utilizza il tipo Pi in azione,
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]]
(nota che qui usiamo l' <:<
operatore di testimonianza del sottotipo di Scala piuttosto che =:=
perché res2.type
e res3.type
sono tipi singleton e quindi più precisi dei tipi che stiamo verificando su RHS).
In pratica, tuttavia, in Scala non si inizierebbe codificando i tipi Sigma e Pi per poi procedere da lì come si farebbe in Agda o Idris. Useremmo invece tipi dipendenti dal percorso, tipi singleton e impliciti direttamente. Puoi trovare numerosi esempi di come questo si svolge in informi: tipi di dimensioni , record estensibili , elenchi H completi , rottami del tuo boilerplate , cerniere generiche ecc. Ecc.
L'unica obiezione che posso vedere è che nella codifica sopra dei tipi Pi richiediamo che i tipi singleton dei valori dipendenti siano esprimibili. Sfortunatamente in Scala questo è possibile solo per valori di tipi di riferimento e non per valori di tipi non di riferimento (esp. Es. Int). Questo è un peccato, ma non una difficoltà intrinseca: il controllo del tipo di Scala rappresenta internamente i tipi singoli di valori non di riferimento e ci sono stati un paio di esperimenti per renderli direttamente esprimibili. In pratica possiamo aggirare il problema con una codifica a livello di tipo abbastanza standard dei numeri naturali .
In ogni caso, non credo che questa leggera restrizione di dominio possa essere usata come un'obiezione allo status di Scala come linguaggio tipizzato in modo dipendente. Se lo è, allora lo stesso si potrebbe dire per il ML dipendente (che consente solo dipendenze dai valori dei numeri naturali), il che sarebbe una conclusione bizzarra.