Qualche motivo per cui scala non supporta esplicitamente i tipi dipendenti?


109

Ci sono percorsi tipi dipendenti e penso che sia possibile esprimere quasi tutte le caratteristiche di tali lingue come Epigram o Agda a Scala, ma mi chiedo perché Scala non supporta questo più esplicitamente come fa molto bene in altri settori (ad esempio , DSL)? Qualcosa che mi manca come "non è necessario"?


3
Ebbene, i progettisti di Scala credono che il Barendregt Lambda Cube non sia l'essenza della teoria dei tipi. Questo potrebbe o non potrebbe essere il motivo.
Jörg W Mittag

8
@ JörgWMittag Cos'è il Cubo Lamda? Una sorta di dispositivo magico?
Ashkan Kh. Nazary

@ ashy_32bit vedi l'articolo di Barendregt "Introduzione ai sistemi di tipi generalizzati" qui: diku.dk/hjemmesider/ansatte/henglein/papers/barendregt1991.pdf
iainmcgin

Risposte:


151

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.Unel 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.typee res3.typesono 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.


8
Miles, grazie per questa risposta molto dettagliata. Sono un po 'curioso di una cosa, però. Nessuno dei tuoi esempi sembra a prima vista particolarmente impossibile da esprimere in Haskell; stai quindi affermando che Haskell è anche un linguaggio tipizzato in modo dipendente?
Jonathan Sterling

8
Ho downvoted perché non riesco a distinguere le tecniche qui in sostanza dalle tecniche descritte in "Faking It" di McBride citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.22.2636 - cioè questi sono modi per simulare tipi dipendenti, non fornirli direttamente.
sclv

2
@sclv Penso che ti sei perso il fatto che Scala ha tipi dipendenti senza alcuna forma di codifica: vedi il primo esempio sopra. Hai perfettamente ragione sul fatto che la mia codifica dei tipi Pi utilizza alcune delle stesse tecniche della carta di Connor, ma da un substrato che include già tipi dipendenti dal percorso e tipi singleton.
Miles Sabin

4
No. Sicuramente puoi avere tipi legati agli oggetti (questa è una conseguenza degli oggetti come moduli). Ma non è possibile eseguire calcoli su questi tipi senza utilizzare testimoni a livello di valore. Infatti =: = stesso è un testimone a livello di valore! Stai ancora fingendo, proprio come devi fare a Haskell, o forse di più.
sclv

9
=: = Di Scala non è a livello di valore, è un costruttore di tipi - un valore per questo è qui: github.com/scala/scala/blob/v2.10.3/src/library/scala/… , e non sembra particolarmente diverso da un testimone per una proposizione di uguaglianza in linguaggi tipizzati in modo dipendente come Agda e Idris: refl. (Vedere www2.tcs.ifi.lmu.de/~abel/Equality.pdf sezione 2 e eb.host.cs.st-andrews.ac.uk/writings/idris-tutorial.pdf sezione 8.1, rispettivamente.)
pdxleif

6

Presumo che sia perché (come so per esperienza, avendo usato i tipi dipendenti nell'assistente di prova Coq, che li supporta completamente ma non in modo molto conveniente) i tipi dipendenti sono una caratteristica del linguaggio di programmazione molto avanzata che è davvero difficile da avere ragione e può causare un aumento esponenziale della complessità nella pratica. Sono ancora un argomento di ricerca informatica.


saresti così gentile da fornirmi alcune basi teoriche sui tipi dipendenti (un collegamento forse)?
Ashkan Kh. Nazary

3
@ ashy_32bit se riesci ad accedere a "Argomenti avanzati in tipi e linguaggi di programmazione" di Benjamin Pierce, c'è un capitolo in questo che fornisce una ragionevole introduzione ai tipi dipendenti. Potresti anche leggere alcuni articoli di Conor McBride che ha un particolare interesse per i tipi dipendenti nella pratica piuttosto che in teoria.
iainmcgin

3

Credo che i tipi dipendenti dal percorso di Scala possano rappresentare solo i tipi Σ, ma non i tipi Π. Questo:

trait Pi[T] { type U }

non è esattamente un tipo Π. Per definizione, il tipo Π, o prodotto dipendente, è una funzione il cui tipo di risultato dipende dal valore dell'argomento, che rappresenta il quantificatore universale, cioè ∀x: A, B (x). Nel caso precedente, però, dipende solo dal tipo T, ma non da qualche valore di questo tipo. Il tratto Pi stesso è un tipo Σ, un quantificatore esistenziale, cioè ∃x: A, B (x). L'auto-riferimento dell'oggetto in questo caso agisce come variabile quantificata. Quando viene passato come parametro implicito, tuttavia, si riduce a una normale funzione di tipo, poiché viene risolta in base al tipo. La codifica per il prodotto dipendente in Scala può essere simile alla seguente:

trait Sigma[T] {
  val x: T
  type U //can depend on x
}

// (t: T) => (∃ mapping(x, U), x == t) => (u: U); sadly, refinement won't compile
def pi[T](t: T)(implicit mapping: Sigma[T] { val x = t }): mapping.U 

Il pezzo mancante qui è la capacità di vincolare staticamente il campo x al valore atteso t, formando effettivamente un'equazione che rappresenta la proprietà di tutti i valori che abitano il tipo T. Insieme ai nostri tipi Σ, usati per esprimere l'esistenza di un oggetto con una data proprietà, il si forma la logica, in cui la nostra equazione è un teorema da dimostrare.

In una nota a margine, nel caso reale il teorema può essere altamente non banale, fino al punto in cui non può essere derivato automaticamente dal codice o risolto senza uno sforzo significativo. Si può persino formulare l'ipotesi di Riemann in questo modo, solo per scoprire che la firma è impossibile da implementare senza dimostrarla effettivamente, ripetendo all'infinito o generando un'eccezione.


1
Miles Sabin ha mostrato sopra un esempio di utilizzo Piper creare tipi a seconda dei valori.
Missingfaktor

Nell'esempio, depListestrae il tipo Uda Pi[T], selezionato per tipo (non valore) di t. Questo tipo è solo un tipo singleton, attualmente disponibile sugli oggetti singleton di Scala e che rappresenta i loro valori esatti. L'esempio crea un'implementazione Piper tipo di oggetto singleton, accoppiando così il tipo con il valore come nel tipo Σ. Il tipo Π, d'altra parte, è una formula che corrisponde alla struttura del suo parametro di input. Forse, Scala non li ha perché i tipi Π richiedono che ogni tipo di parametro sia GADT e Scala non distingue i GADT dagli altri tipi.
P. Frolov

Ok, sono un po 'confuso. pi.UNell'esempio di Miles non conta come tipo dipendente? È sul valore pi.
missingfaktor

2
In effetti conta come tipo dipendente, ma ci sono diversi gusti di questi: Σ-type ("esiste x tale che P (x)", dal punto di vista logico) e Π-type ("per tutte le x, P (x)") . Come hai notato, il tipo pi.Udipende dal valore di pi. Il problema che impedisce trait Pi[T]di diventare un tipo Π è che non possiamo renderlo dipendente dal valore di un argomento arbitrario (ad esempio, tin depList) senza sollevare quell'argomento a livello di tipo.
P. Frolov

1

La domanda riguardava l'utilizzo più diretto della funzionalità tipizzata in modo dipendente e, a mio parere, ci sarebbe stato un vantaggio nell'avere un approccio di digitazione dipendente più diretto rispetto a quello che offre Scala.
Le risposte attuali cercano di argomentare la domanda sul tipo a livello teorico. Voglio dare una svolta più pragmatica. Questo potrebbe spiegare perché le persone sono divise sul livello di supporto dei tipi dipendenti nel linguaggio Scala. Potremmo avere in mente definizioni un po 'diverse. (per non dire che uno ha ragione e uno ha torto).

Questo non è un tentativo di rispondere alla domanda su quanto sarebbe facile trasformare Scala in qualcosa come Idris (immagino molto difficile) o scrivere una libreria che offra un supporto più diretto per capacità simili a Idris (come singletonscerca di essere in Haskell).

Invece, voglio sottolineare la differenza pragmatica tra Scala e un linguaggio come Idris.
Cosa sono i bit di codice per le espressioni a livello di valore e tipo? Idris usa lo stesso codice, Scala usa codice molto diverso.

Scala (in modo simile a Haskell) potrebbe essere in grado di codificare molti calcoli a livello di tipo. Questo è mostrato da biblioteche come shapeless. Queste librerie lo fanno usando alcuni trucchi davvero impressionanti e intelligenti. Tuttavia, il loro codice a livello di tipo è (attualmente) abbastanza diverso dalle espressioni a livello di valore (trovo che il divario sia un po 'più vicino in Haskell). Idris consente di utilizzare l'espressione del livello di valore sul livello di tipo COSÌ COM'È.

L'ovvio vantaggio è il riutilizzo del codice (non è necessario codificare le espressioni a livello di tipo separatamente dal livello di valore se sono necessarie in entrambi i punti). Dovrebbe essere molto più semplice scrivere codice a livello di valore. Dovrebbe essere più facile non avere a che fare con hack come i singleton (per non parlare del costo delle prestazioni). Non hai bisogno di imparare due cose, impari una cosa. A livello pragmatico, finiamo per aver bisogno di meno concetti. Digita sinonimi, digita famiglie, funzioni, ... che ne dici solo di funzioni? A mio parere, questi vantaggi unificanti vanno molto più in profondità e sono più che convenienza sintattica.

Considera il codice verificato. Vedi:
https://github.com/idris-lang/Idris-dev/blob/v1.3.0/libs/contrib/Interfaces/Verified.idr
Il controllo del tipo verifica le prove delle leggi monadiche / funtore / applicative e le prove sono circa effettive implementazioni di monade / funtore / applicativo e non un equivalente a livello di tipo codificato che può essere lo stesso o non lo stesso. La grande domanda è cosa stiamo dimostrando?

Lo stesso posso fare usando trucchi di codifica intelligenti (vedi quanto segue per la versione Haskell, non ne ho visto uno per Scala)
https://blog.jle.im/entry/verified-instances-in-haskell.html
https: // github.com/rpeszek/IdrisTddNotes/wiki/Play_FunctorLaws
tranne i tipi sono così complicati che è difficile vedere le leggi, le espressioni a livello di valore vengono convertite (automaticamente ma comunque) per digitare cose di livello e devi fidarti anche di quella conversione . C'è spazio per errori in tutto questo che sfida lo scopo del compilatore che funge da assistente di prova.

(MODIFICATO 2018.8.10) Parlando di assistenza per le prove, ecco un'altra grande differenza tra Idris e Scala. Non c'è nulla in Scala (o Haskell) che possa impedire di scrivere dimostrazioni divergenti:

case class Void(underlying: Nothing) extends AnyVal //should be uninhabited
def impossible() : Void = impossible()

mentre Idris ha totalparole chiave che impediscono la compilazione di codice come questo.

Una libreria Scala che cerchi di unificare valore e codice a livello di tipo (come Haskell singletons) sarebbe un test interessante per il supporto di Scala dei tipi dipendenti. Questa libreria può essere realizzata molto meglio in Scala a causa dei tipi dipendenti dal percorso?

Sono troppo nuovo per Scala per rispondere a questa domanda da solo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.