Perché non scrivere in modo dipendente?


161

Ho visto diverse fonti ribadire l'opinione che "Haskell sta gradualmente diventando un linguaggio tipicamente dipendente". L'implicazione sembra essere che con sempre più estensioni del linguaggio, Haskell sta andando alla deriva in quella direzione generale, ma non c'è ancora.

Ci sono fondamentalmente due cose che vorrei sapere. Il primo è, molto semplicemente, che cosa significa "essere un linguaggio dipendentemente tipizzato" in realtà significa ? (Speriamo senza essere troppo tecnico a riguardo.)

La seconda domanda è ... qual è lo svantaggio? Voglio dire, la gente sa che stiamo andando in quel modo, quindi ci deve essere qualche vantaggio. Eppure, non siamo ancora arrivati, quindi ci deve essere qualche svantaggio che impedisce alle persone di andare fino in fondo. Ho l'impressione che il problema sia un forte aumento della complessità. Ma, non capendo davvero cosa sia la digitazione dipendente, non lo so per certo.

Quello che faccio sapere è che ogni volta che inizio a leggere su un linguaggio di programmazione dipendente-digitato, il testo è del tutto incomprensibile ... Presumibilmente questo è il problema. (?)


10
In parole povere, puoi scrivere tipi che dipendono da termini (calcoli). Questo è sufficiente per specificare i tipi su ogni aspetto del programma, e quindi significa che il sistema dei tipi è in grado di specificare il programma completo. Il problema è che, poiché i tipi dipendono dai calcoli, la verifica dei tipi è molto più difficile da eseguire (impossibile in generale).
GManNickG,

27
@GManNickG: il controllo del tipo è del tutto possibile. L' inferenza del tipo è un'altra questione, ma poi le varie estensioni di GHC hanno da tempo abbandonato l'idea che dovrebbe essere possibile inferire tutti i tipi.
CA McCann,

7
Se ho capito bene, lo svantaggio è che fare una digitazione dipendente corretta (ad esempio, in un modo che sia sia utilizzabile che fondato) è difficile e non sappiamo ancora abbastanza.
comingstorm

1
@CAMcCann: Sì, errore mio.
GManNickG,

4
Non credo che qualcuno abbia sottolineato l'unico grande svantaggio pragmatico: scrivere prove che tutto il tuo codice sia corretto è abbastanza follemente noioso. Poiché non puoi fare automaticamente l'inferenza del tipo (corrisponde al teorema che dimostra una logica "hella potente"), devi scrivere le annotazioni per il tuo programma sotto forma di prove. Questo ovviamente diventa fastidioso e difficile da fare dopo un po ', specialmente per la magia monadica più elaborata che le persone fanno tipicamente a Haskell. Il più vicino che stiamo arrivando in questi giorni sono le lingue che fanno la maggior parte di questo per noi o ci danno una buona serie di primitivi.
Kristopher Micinski,

Risposte:


21

La tipizzazione dipendente è in realtà solo l'unificazione del valore e dei livelli di tipo, quindi puoi parametrizzare i valori sui tipi (già possibile con classi di tipi e polimorfismo parametrico in Haskell) e puoi parametrizzare i tipi su valori (non, a rigor di termini, possibili ancora in Haskell , anche se DataKindssi avvicina molto).

Modifica: Apparentemente, da questo punto in poi, ho sbagliato (vedi il commento di @ pigworker). Conserverò il resto di questo come una registrazione dei miti che sono stato nutrito. : P


Il problema con il passaggio alla tipizzazione completamente dipendente, da quello che ho sentito, è che avrebbe spezzato la restrizione di fase tra i livelli di tipo e valore che consente a Haskell di essere compilato in un codice macchina efficiente con tipi cancellati. Con il nostro attuale livello di tecnologia, un linguaggio tipicamente dipendente deve passare attraverso un interprete ad un certo punto (o immediatamente, o dopo essere stato compilato in bytecode tipicamente dipendente o simile).

Questa non è necessariamente una restrizione fondamentale, ma non sono personalmente a conoscenza di alcuna ricerca attuale che sembra promettente a questo proposito ma che non è già diventata GHC. Se qualcun altro ne sapesse di più, sarei felice di essere corretto.


46
Quello che dici è quasi del tutto falso. Non ti sto completamente incolpando: ripete i miti standard come un fatto. Il linguaggio di Edwin Brady, Idris, esegue la cancellazione dei tipi (poiché nessun comportamento di runtime dipende dai tipi) e genera una codifica del supercombinatore sollevata da lambda abbastanza standard da cui viene generato il codice utilizzando tecniche G-machine di serie.
Pigworker,

3
Come nota a margine, qualcuno mi ha recentemente segnalato questo documento . Da quello che posso dire, renderebbe Haskell di tipo dipendente (ovvero, il linguaggio a livello di tipo verrebbe tipizzato in modo dipendente), il che è il più vicino possibile per vederci arrivare presto.
Ptharien's Flame,

8
Sì, quel documento fa di tutto per mostrare come rendere i tipi dipendenti da elementi a livello di tipo (e per eliminare la distinzione tipo / tipo). Un follow-up plausibile, già in discussione, è quello di consentire tipi di funzione dipendenti effettivi, ma limitare i loro argomenti al frammento del linguaggio che può esistere sia nei livelli di valore che di tipo (ora non banali grazie alla promozione del tipo di dati). Ciò eliminerebbe la necessità della costruzione singleton che attualmente rende "fingendolo" più complesso di quanto desiderabile. Ci stiamo avvicinando costantemente alla cosa reale.
Pigworker,

13
Ci sono molte domande pragmatiche, retrofitting di tipi dipendenti su Haskell. Una volta che abbiamo questa forma ristretta di spazio di funzioni dipendenti, affrontiamo ancora la questione di come ingrandire il frammento del linguaggio dei valori consentito a livello di tipo e quale dovrebbe essere la sua teoria equazionale (come vogliamo che 2 + 2 essere 4 e simili). Ci sono un sacco di problemi difficili (ad esempio, in basso) che i linguaggi tipicamente dipendenti da zero progettano da zero.
Pigworker,

2
@pigworker C'è un sottoinsieme pulito di Haskell che è totale? Se è così, non potremmo semplicemente usarlo per il "frammento del linguaggio che può esistere sia nei livelli di valore che di tipo"? In caso contrario, cosa sarebbe necessario per produrne uno?
Ptharien's Flame,

223

Dipendentemente digitato Haskell, ora?

Haskell è, in piccola parte, un linguaggio tipicamente dipendente. Esiste una nozione di dati a livello di tipo, ora più sensibilmente digitati grazie a DataKinds, e vi sono alcuni mezzi ( GADTs) per fornire una rappresentazione di runtime ai dati a livello di tipo. Quindi, i valori delle cose di runtime vengono effettivamente visualizzati in tipi , che è ciò che significa che una lingua deve essere tipizzata in modo dipendente.

I tipi di dati semplici vengono promossi a livello di tipo, in modo che i valori in essi contenuti possano essere utilizzati nei tipi. Da qui l'esempio archetipico

data Nat = Z | S Nat

data Vec :: Nat -> * -> * where
  VNil   :: Vec Z x
  VCons  :: x -> Vec n x -> Vec (S n) x

diventa possibile e con esso definizioni come

vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil         VNil         = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)

che è bello. Nota che la lunghezza nè una cosa puramente statica in quella funzione, assicurando che i vettori di input e output abbiano la stessa lunghezza, anche se quella lunghezza non ha alcun ruolo nell'esecuzione di vApply. Al contrario, è molto più complicato (vale a dire, impossibile) per implementare la funzione che fa ncopie di un dato x(che sarebbe purea vApply's <*>)

vReplicate :: x -> Vec n x

perché è fondamentale sapere quante copie eseguire in fase di esecuzione. Inserisci i singoli.

data Natty :: Nat -> * where
  Zy :: Natty Z
  Sy :: Natty n -> Natty (S n)

Per qualsiasi tipo promotabile, possiamo costruire la famiglia singleton, indicizzata sul tipo promosso, abitata da duplicati di runtime dei suoi valori. Natty nè il tipo di copie runtime del tipo-livello n :: Nat. Ora possiamo scrivere

vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy     x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)

Quindi lì hai un valore a livello di tipo associato a un valore di runtime: l'ispezione della copia di runtime raffina la conoscenza statica del valore a livello di tipo. Anche se termini e tipi sono separati, possiamo lavorare in modo dipendente dal tipo usando la costruzione singleton come una specie di resina epossidica, creando legami tra le fasi. È molto lontano dal consentire espressioni di runtime arbitrarie in tipi, ma non è niente.

Cosa c'è di cattivo? Cosa manca?

Facciamo un po 'di pressione su questa tecnologia e vediamo cosa inizia a vacillare. Potremmo avere l'idea che i singoli dovrebbero essere gestibili in modo un po 'più implicito

class Nattily (n :: Nat) where
  natty :: Natty n
instance Nattily Z where
  natty = Zy
instance Nattily n => Nattily (S n) where
  natty = Sy natty

permettendoci di scrivere, diciamo,

instance Nattily n => Applicative (Vec n) where
  pure = vReplicate natty
  (<*>) = vApply

Funziona, ma ora significa che il nostro Nattipo originale ha generato tre copie: una specie, una famiglia singleton e una classe singleton. Abbiamo un processo piuttosto ingombrante per lo scambio di Natty nvalori e Nattily ndizionari espliciti . Inoltre, Nattynon lo è Nat: abbiamo una sorta di dipendenza dai valori di runtime, ma non dal tipo a cui abbiamo pensato per la prima volta. Nessun linguaggio tipicamente dipendente rende i tipi dipendenti così complicati!

Nel frattempo, sebbene Natpossa essere promosso, Vecnon può. Non è possibile indicizzare per tipo indicizzato. Completamente su linguaggi tipicamente dipendenti non impone tale restrizione, e nella mia carriera di show-off tipicamente dipendente, ho imparato a includere esempi di indicizzazione a due livelli nei miei discorsi, solo per insegnare alle persone che hanno fatto l'indicizzazione a uno strato difficile ma possibile non aspettarsi che mi pieghi come un castello di carte. Qual è il problema? Uguaglianza. I GADT funzionano traducendo i vincoli che si ottengono implicitamente quando si assegna a un costruttore un tipo di ritorno specifico in esplicite richieste equazionali. Come questo.

data Vec (n :: Nat) (x :: *)
  = n ~ Z => VNil
  | forall m. n ~ S m => VCons x (Vec m x)

In ciascuna delle nostre due equazioni, entrambe le parti hanno gentile Nat.

Ora prova la stessa traduzione per qualcosa di indicizzato sui vettori.

data InVec :: x -> Vec n x -> * where
  Here :: InVec z (VCons z zs)
  After :: InVec z ys -> InVec z (VCons y ys)

diventa

data InVec (a :: x) (as :: Vec n x)
  = forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
  | forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)

e ora formiamo vincoli equazionali tra as :: Vec n xe VCons z zs :: Vec (S m) xdove le due parti hanno tipi sintatticamente distinti (ma dimostrabilmente uguali). Il core GHC non è attualmente attrezzato per un simile concetto!

Cos'altro manca? Bene, la maggior parte di Haskell manca dal livello del tipo. Il linguaggio dei termini che puoi promuovere ha solo variabili e costruttori non GADT, davvero. Una volta che li hai, il type familymacchinario ti consente di scrivere programmi a livello di tipo: alcuni di questi potrebbero essere abbastanza simili a funzioni che considereresti di scrivere a livello di termine (ad esempio, equipaggiare Natcon l'aggiunta, in modo da poter dare un buon tipo da aggiungere Vec) , ma è solo una coincidenza!

Un'altra cosa che manca, in pratica, è una libreria che si avvale delle nostre nuove abilità per indicizzare i tipi in base ai valori. Cosa fanno Functor e Monaddiventano in questo nuovo mondo coraggioso? Ci sto pensando, ma c'è ancora molto da fare.

Esecuzione di programmi a livello di tipo

Haskell, come la maggior parte dei linguaggi di programmazione tipicamente dipendenti, ha due semantiche operative. C'è il modo in cui il sistema di runtime esegue i programmi (solo espressioni chiuse, dopo la cancellazione del tipo, altamente ottimizzato) e poi c'è il modo in cui il typechecker esegue i programmi (le tue famiglie di tipi, il tuo "tipo di classe Prolog", con espressioni aperte). Per Haskell, normalmente non si mescolano i due, perché i programmi in esecuzione sono in lingue diverse. Le lingue tipizzate in modo dipendente hanno modelli separati di runtime ed esecuzione statica per la stessa lingua di programmi, ma non preoccuparti, il modello di runtime ti consente ancora di digitare la cancellazione e, in effetti, la prova di cancellazione: questo è ciò che l' estrazione di Coqil meccanismo ti dà; questo è almeno ciò che fa il compilatore di Edwin Brady (anche se Edwin cancella valori duplicati inutilmente, nonché tipi e prove). La distinzione di fase potrebbe non essere più una distinzione della categoria sintattica , ma è viva e vegeta.

Le lingue tipicamente dipendenti, essendo totali, consentono al typechecker di eseguire programmi liberi dalla paura di qualcosa di peggio di una lunga attesa. Mentre Haskell viene tipizzato in modo più dipendente, ci troviamo di fronte alla domanda su quale dovrebbe essere il suo modello di esecuzione statica? Un approccio potrebbe essere quello di limitare l'esecuzione statica alle funzioni totali, il che ci consentirebbe la stessa libertà di esecuzione, ma potrebbe costringerci a fare distinzioni (almeno per il codice a livello di tipo) tra dati e codata, in modo da poter dire se imporre la risoluzione o la produttività. Ma questo non è l'unico approccio. Siamo liberi di scegliere un modello di esecuzione molto più debole che è riluttante a eseguire programmi, al costo di far uscire meno equazioni solo dal calcolo. E in effetti, è quello che fa GHC. Le regole di digitazione per GHC core non menzionano la corsa programmi, ma solo per verificare l'evidenza delle equazioni. Quando si traduce in core, il risolutore di vincoli di GHC cerca di eseguire i programmi a livello di tipo, generando una piccola scia argentata di prove che una determinata espressione è uguale alla sua forma normale. Questo metodo di generazione delle prove è un po 'imprevedibile e inevitabilmente incompleto: combatte ad esempio con una ricorsione dall'aspetto spaventoso, e probabilmente è saggio. Una cosa di cui non dobbiamo preoccuparci è l'esecuzione dei IO calcoli nel typechecker: ricorda che il typechecker non deve dare launchMissileslo stesso significato del sistema runtime!

Cultura Hindley-Milner

Il sistema di tipo Hindley-Milner raggiunge la straordinaria coincidenza di quattro distinzioni distinte, con lo sfortunato effetto collaterale culturale che molte persone non riescono a vedere la distinzione tra le distinzioni e supporre che la coincidenza sia inevitabile! Di cosa sto parlando?

  • termini vs tipi
  • cose scritte in modo esplicito contro cose scritte in modo implicito
  • presenza in fase di esecuzione vs cancellazione prima della fase di esecuzione
  • astrazione non dipendente vs quantificazione dipendente

Siamo abituati a scrivere termini e lasciare inferire i tipi ... e poi cancellati. Siamo abituati a quantificare le variabili di tipo con l'astrazione del tipo corrispondente e l'applicazione che avviene silenziosamente e staticamente.

Non devi allontanarti troppo dalla vaniglia Hindley-Milner prima che queste distinzioni vengano fuori allineamento, e non è male . Per cominciare, possiamo avere tipi più interessanti se siamo disposti a scriverli in alcuni punti. Nel frattempo, non dobbiamo scrivere dizionari di classe di tipo quando utilizziamo funzioni sovraccaricate, ma quei dizionari sono certamente presenti (o incorporati) in fase di esecuzione. In linguaggi tipicamente dipendenti, ci aspettiamo di cancellare più dei semplici tipi in fase di esecuzione, ma (come con le classi di tipi) che alcuni valori implicitamente dedotti non verranno cancellati. Ad esempio, vReplicatel'argomento numerico è spesso inferibile dal tipo del vettore desiderato, ma dobbiamo ancora conoscerlo in fase di esecuzione.

Quali scelte di design linguistico dovremmo rivedere perché queste coincidenze non valgono più? Ad esempio, è giusto che Haskell non fornisca alcun modo per creare un'istanza forall x. tesplicita di un quantificatore? Se il typechecker non può indovinare xunificando t, non abbiamo altro modo di dire cosa xdeve essere.

Più in generale, non possiamo trattare l '"inferenza di tipo" come un concetto monolitico di cui abbiamo tutto o niente. Per cominciare, dobbiamo separare l'aspetto di "generalizzazione" (regola "let" di Milner), che si basa fortemente sulla limitazione di quali tipi esistono per garantire che una macchina stupida possa indovinarne uno, dall'aspetto di "specializzazione" (var di Milner "regola) che è efficace quanto il tuo risolutore di vincoli. Possiamo aspettarci che i tipi di livello superiore diventeranno più difficili da dedurre, ma che le informazioni sui tipi interni rimarranno abbastanza facili da propagare.

I prossimi passi per Haskell

Stiamo vedendo che i livelli di tipo e tipo diventano molto simili (e condividono già una rappresentazione interna in GHC). Potremmo anche unirli. Sarebbe divertente * :: *se potessimo: abbiamo perso la solidità logica molto tempo fa, quando consentivamo il fondo, ma la solidità del tipo è di solito un requisito più debole. Dobbiamo controllare. Se dobbiamo avere livelli distinti di tipo, tipo, ecc., Possiamo almeno assicurarci che ogni cosa a livello di tipo e superiore possa sempre essere promossa. Sarebbe bello solo riutilizzare il polimorfismo che già abbiamo per i tipi, piuttosto che reinventare il polimorfismo a livello gentile.

Dovremmo semplificare e generalizzare l'attuale sistema di vincoli consentendo equazioni eterogenee ina ~ b cui i tipi di ae bnon sono sintatticamente identici (ma possono essere dimostrati uguali). È una vecchia tecnica (nella mia tesi, del secolo scorso) che rende la dipendenza molto più facile da affrontare. Saremmo in grado di esprimere vincoli sulle espressioni nei GADT, e quindi allentare le restrizioni su ciò che può essere promosso.

Dobbiamo eliminare la necessità per la costruzione singleton introducendo un tipo di funzione dipendente pi x :: s -> t. Una funzione con un tale tipo potrebbe essere applicata esplicitamente a qualsiasi espressione di tipo sche vive nell'intersezione del tipo e dei linguaggi dei termini (quindi, variabili, costruttori, con altri a venire in seguito). La lambda e l'applicazione corrispondenti non verrebbero cancellate in fase di esecuzione, quindi saremmo in grado di scrivere

vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z     x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)

senza sostituire Natcon Natty. Il dominio di pipuò essere di qualsiasi tipo promuovibile, quindi se i GADT possono essere promossi, possiamo scrivere sequenze di quantificatori dipendenti (o "telescopi" come li chiamavano de Briuijn)

pi n :: Nat -> pi xs :: Vec n x -> ...

di qualunque lunghezza abbiamo bisogno.

Il punto di questi passaggi è eliminare la complessità lavorando direttamente con strumenti più generali, invece di accontentarsi di strumenti deboli e codifiche goffe. L'attuale buy-in parziale rende i vantaggi dei tipi dipendenti di Haskell più costosi di quanto debbano essere.

Troppo difficile?

I tipi dipendenti rendono molte persone nervose. Mi rendono nervoso, ma mi piace essere nervoso, o almeno trovo difficile non essere nervoso comunque. Ma non aiuta che ci sia una tale nebbia di ignoranza attorno all'argomento. In parte ciò è dovuto al fatto che tutti abbiamo ancora molto da imparare. Ma è noto che i sostenitori di approcci meno radicali alimentano la paura dei tipi dipendenti senza assicurarsi sempre che i fatti siano interamente con loro. Non nominerò i nomi. Questi "indecidibili controlli dei caratteri", "Turing incompleti", "nessuna distinzione di fase", "nessuna cancellazione di tipo", "prove ovunque", ecc., Persistono miti, anche se sono spazzatura.

Non è certo il caso che i programmi tipicamente dipendenti debbano sempre essere dimostrati corretti. Si può migliorare l'igiene di base dei propri programmi, imponendo ulteriori invarianti in tipi senza andare fino in fondo a una specifica completa. Piccoli passi in questa direzione spesso portano a garanzie molto più forti con pochi o nessun obbligo di prova aggiuntivo. Non è vero che i programmi scritti in modo dipendente siano inevitabilmente pieni di prove, anzi di solito prendo la presenza di eventuali prove nel mio codice come spunto per mettere in discussione le mie definizioni .

Perché, come con ogni aumento di articolazione, diventiamo liberi di dire cose nuove, oltre che giuste. Ad esempio, ci sono molti modi scadenti per definire alberi di ricerca binari, ma ciò non significa che non ci sia un buon modo . È importante non presumere che le brutte esperienze non possano essere migliorate, anche se ammette l'ego di ammetterlo. La progettazione di definizioni dipendenti è una nuova abilità che richiede apprendimento, ed essere un programmatore Haskell non ti rende automaticamente un esperto! E anche se alcuni programmi sono fallo, perché negheresti ad altri la libertà di essere onesti?

Perché preoccuparsi ancora di Haskell?

Mi piacciono molto i tipi dipendenti, ma la maggior parte dei miei progetti di hacking sono ancora in Haskell. Perché? Haskell ha classi di tipi. Haskell ha utili librerie. Haskell ha un trattamento praticabile (sebbene tutt'altro che ideale) di programmazione con effetti. Haskell ha un compilatore di forza industriale. I linguaggi tipicamente dipendenti sono in una fase molto più precoce nella crescita della comunità e delle infrastrutture, ma ci arriveremo, con un vero passaggio generazionale in ciò che è possibile, ad esempio, attraverso metaprogrammazione e generici di tipi di dati. Ma devi solo guardarti intorno cosa fanno le persone a seguito dei passi di Haskell verso i tipi dipendenti per vedere che ci sono molti vantaggi da ottenere spingendo avanti anche l'attuale generazione di lingue.


6
Davvero non mi importa ancora delle cose di DataKinds. Soprattutto perché voglio fare qualcosa di simile: fmap read getLine >>= \n -> vReplicate n 0. Come notate, Nattyè molto lontano da questo. Inoltre, vReplicate dovrebbe essere traducibile in un array di memoria reale, qualcosa del genere newtype SVector n x = SVector (Data.Vector.Vector x), dove nha tipo Nat(o simile). Forse un altro punto di dimostrazione per uno "show-off tipicamente dipendente?"
John L

7
Potresti dire cosa hai in mente per un trattamento ideale della programmazione con effetti?
Steven Shaw,

6
Grazie per l'ottima scrittura. Mi piacerebbe vedere alcuni esempi di codice tipicamente dipendente in cui alcuni dati hanno origine al di fuori del programma (ad esempio, letti da un file), per avere un'idea di come apparirebbero i valori per i tipi in una tale impostazione. Ho la sensazione che tutti gli esempi riguardino vettori (implementati come elenchi) con dimensioni staticamente note.
martedì

4
@pigworker Prendi "nessuna distinzione di fase" come mito (gli altri sono d'accordo che sono miti). Ma non hai smontato questo in articoli e discorsi che ho visto, e nel frattempo un'altra persona che rispetto mi dice "la teoria dei tipi dipendenti è diversa da un tipico compilatore perché non possiamo separare in modo significativo le fasi di verifica, compilazione ed esecuzione dei tipi. " (vedi l'ultimo post di Andrej dell'8 novembre 2012) Nella mia esperienza "fingendolo" a volte almeno confondiamo la distinzione di fase, sebbene non sia necessario cancellarla. Potresti approfondire, se non qui, poi altrove, su questo problema?
sclv,

4
@sclv Il mio lavoro non ha preso di mira in particolare il mito "nessuna distinzione di fase", ma quello di altri. Consiglio il rifiuto "Phase Distintions in the Compilation of Epigram", di James McKinna e Edwin Brady, come un buon punto di partenza. Ma vedi anche lavori molto più vecchi sull'estrazione del programma in Coq. La valutazione a termini aperti effettuata dal typechecker è completamente separata dall'esecuzione tramite estrazione in ML, ed è chiaro che l'estrazione elimina tipi e prove.
Maiale

20

John è un altro malinteso comune sui tipi dipendenti: che non funzionano quando i dati sono disponibili solo in fase di esecuzione. Ecco come è possibile fare l'esempio getLine:

data Some :: (k -> *) -> * where
  Like :: p x -> Some p

fromInt :: Int -> Some Natty
fromInt 0 = Like Zy
fromInt n = case fromInt (n - 1) of
  Like n -> Like (Sy n)

withZeroes :: (forall n. Vec n Int -> IO a) -> IO a
withZeroes k = do
  Like n <- fmap (fromInt . read) getLine
  k (vReplicate n 0)

*Main> withZeroes print
5
VCons 0 (VCons 0 (VCons 0 (VCons 0 (VCons 0 VNil))))

Modifica: Hm, avrebbe dovuto essere un commento alla risposta del maiale. Chiaramente non riesco a SO.


La tua prima frase sembra un po 'strana; Direi che il punto di tipi dipendenti è che essi fanno il lavoro quando i dati sono disponibili solo a run-time. Tuttavia, questa tecnica in stile CPS non è la stessa. Supponi di avere una funzione Vec Zy -> IO String. Non puoi usarlo con withZeroes, perché il tipo Zynon può essere unificato con forall n. Forse puoi aggirare il problema per uno o due casi speciali, ma ti sfugge rapidamente di mano.
John L,

La chiave quando si prende un valore semplicemente digitato (come String da getLine) e lo si trasforma in qualcosa con un tipo più forte (come un Natty n sopra) è che devi convincere il verificatore di tipo che stai facendo i necessari controlli dinamici. Nel tuo esempio stai leggendo un numero arbitrario, quindi forall nha senso. Restrizioni più precise possono essere implementate allo stesso modo. Hai un esempio migliore di Vec Zy(il programma dovrebbe comunque gestire l'utente inserendo 5 anziché 0)?
Ulfnorell,

1
Quello che volevo dire con la prima frase è che occasionalmente incontro persone che credono che non puoi usare tipi dipendenti se ottieni i tuoi dati interagendo con il mondo esterno. Il mio punto è che l'unica cosa che devi fare è scrivere un parser tipicamente dipendente, che di solito è semplice.
Ulfnorell,

1
ulfnorell: Scusa, non ero chiaro. Supponiamo che tu abbia una funzione che funzionerà con Vec Zy -> IO Stringun'altra Vec n -> IO Stringe che tu voglia usare la prima solo se il tipo corrisponde. Sì, è possibile, ma i meccanismi per attivarlo sono goffi. E questa è una logica molto semplice; se hai una logica più complessa è peggio. Inoltre, potrebbe essere necessario riscrivere molto codice in CPS. E non hai ancora un'espressione a livello di tipo che dipende da un termine a livello di valore
John L

Ah, capisco cosa stai dicendo. Questo è ciò che serve a Natty, come in vReplicate dove facciamo cose diverse a seconda di n. In effetti, questo può diventare un po 'goffo. Un'alternativa allo stile CPS è di lavorare con esistenziali impacchettato: zeroes :: IO (Some (Flip Vec Int)).
Ulfnorell,

19

pigworker offre un'eccellente discussione sul perché noi dovremmo essere diretti verso tipi dipendenti: (a) Sono impressionanti; (b) in realtà semplificherebbero molto di ciò che Haskell già fa.

Per quanto riguarda il "perché no?" domanda, ci sono un paio di punti penso. Il primo punto è che mentre la nozione di base dietro i tipi dipendenti è facile (consenti ai tipi di dipendere dai valori), le ramificazioni di tale nozione di base sono sia sottili che profonde. Ad esempio, la distinzione tra valori e tipi è ancora viva e vegeta; ma discutere la differenza tra loro diventa lontanopiù sfumato che nel tuo Hindley - Milner o System F. In una certa misura ciò è dovuto al fatto che i tipi dipendenti sono fondamentalmente difficili (ad esempio, la logica del primo ordine è indecidibile). Ma penso che il problema più grande sia proprio la mancanza di un buon vocabolario per catturare e spiegare cosa sta succedendo. Man mano che sempre più persone imparano a conoscere i tipi dipendenti, svilupperemo un vocabolario migliore e così le cose diventeranno più facili da capire, anche se i problemi sottostanti sono ancora difficili.

Il secondo punto ha a che fare con il fatto che Haskell lo sia crescendoverso tipi dipendenti. Poiché stiamo facendo progressi progressivi verso quell'obiettivo, ma senza farlo effettivamente lì, siamo bloccati con un linguaggio che ha patch incrementali oltre a patch incrementali. Lo stesso genere di cose è accaduto in altre lingue quando le nuove idee sono diventate popolari. Java non aveva il polimorfismo (parametrico); e quando alla fine l'hanno aggiunto, è stato ovviamente un miglioramento incrementale con alcune perdite di astrazione e potere paralizzato. Si scopre che mescolare sottotipizzazione e polimorfismo è intrinsecamente difficile; ma non è questo il motivo per cui Java Generics funziona nel modo in cui funziona. Funzionano come fanno a causa del vincolo di un miglioramento incrementale rispetto alle versioni precedenti di Java. Idem, per tornare indietro nel tempo in cui fu inventata la OOP e la gente iniziò a scrivere "obiettivi" C (da non confondere con Objective-C), ecc. Ricorda, C ++ ha iniziato con il pretesto di essere un superset rigoroso di C. L'aggiunta di nuovi paradigmi richiede sempre di definire nuovamente il linguaggio, oppure finire con un po 'di confusione complicata. Il mio punto in tutto ciò è che l'aggiunta di veri e propri tipi dipendenti a Haskell richiederà una certa quantità di sventramento e ristrutturazione del linguaggio, se vogliamo farlo nel modo giusto. Ma è davvero difficile impegnarsi in questo tipo di revisione, mentre i progressi incrementali che stiamo facendo sembrano più economici a breve termine. Davvero, non ci sono molte persone che hackerano GHC, ma c'è una buona quantità di codice legacy da mantenere in vita. Questo è uno dei motivi per cui ci sono così tanti linguaggi spin-off come DDC, Cayenne, Idris, ecc. Il C ++ iniziò con il pretesto di essere un superset rigoroso di C. L'aggiunta di nuovi paradigmi richiede sempre di definire nuovamente il linguaggio, oppure finire con un po 'di complicazioni. Il mio punto in tutto ciò è che l'aggiunta di veri e propri tipi dipendenti a Haskell richiederà una certa quantità di sventramento e ristrutturazione del linguaggio, se vogliamo farlo nel modo giusto. Ma è davvero difficile impegnarsi in questo tipo di revisione, mentre i progressi incrementali che stiamo facendo sembrano più economici a breve termine. Davvero, non ci sono molte persone che hackerano GHC, ma c'è una buona quantità di codice legacy da mantenere in vita. Questo è uno dei motivi per cui ci sono così tanti linguaggi spin-off come DDC, Cayenne, Idris, ecc. Il C ++ iniziò con il pretesto di essere un superset rigoroso di C. L'aggiunta di nuovi paradigmi richiede sempre di definire nuovamente il linguaggio, oppure finire con un po 'di complicazioni. Il mio punto in tutto ciò è che l'aggiunta di veri e propri tipi dipendenti a Haskell richiederà una certa quantità di sventramento e ristrutturazione del linguaggio, se vogliamo farlo nel modo giusto. Ma è davvero difficile impegnarsi in questo tipo di revisione, mentre i progressi incrementali che stiamo facendo sembrano più economici a breve termine. Davvero, non ci sono molte persone che hackerano GHC, ma c'è una buona quantità di codice legacy da mantenere in vita. Questo è uno dei motivi per cui ci sono così tanti linguaggi spin-off come DDC, Cayenne, Idris, ecc. oppure finendo con qualche pasticcio complicato. Il mio punto in tutto ciò è che l'aggiunta di veri e propri tipi dipendenti a Haskell richiederà una certa quantità di sventramento e ristrutturazione del linguaggio, se vogliamo farlo nel modo giusto. Ma è davvero difficile impegnarsi in questo tipo di revisione, mentre i progressi incrementali che stiamo facendo sembrano più economici a breve termine. Davvero, non ci sono molte persone che hackerano GHC, ma c'è una buona quantità di codice legacy da mantenere in vita. Questo è uno dei motivi per cui ci sono così tanti linguaggi spin-off come DDC, Cayenne, Idris, ecc. oppure finendo con qualche pasticcio complicato. Il mio punto in tutto ciò è che l'aggiunta di veri e propri tipi dipendenti a Haskell richiederà una certa quantità di sventramento e ristrutturazione del linguaggio, se vogliamo farlo nel modo giusto. Ma è davvero difficile impegnarsi in questo tipo di revisione, mentre i progressi incrementali che stiamo facendo sembrano più economici a breve termine. Davvero, non ci sono molte persone che hackerano GHC, ma c'è una buona quantità di codice legacy da mantenere in vita. Questo è uno dei motivi per cui ci sono così tanti linguaggi spin-off come DDC, Cayenne, Idris, ecc. È davvero difficile impegnarsi in questo tipo di revisione, mentre i progressi incrementali che stiamo facendo sembrano più economici a breve termine. Davvero, non ci sono molte persone che hackerano GHC, ma c'è una buona quantità di codice legacy da mantenere in vita. Questo è uno dei motivi per cui ci sono così tanti linguaggi spin-off come DDC, Cayenne, Idris, ecc. È davvero difficile impegnarsi in questo tipo di revisione, mentre i progressi incrementali che stiamo facendo sembrano più economici a breve termine. Davvero, non ci sono molte persone che hackerano GHC, ma c'è una buona quantità di codice legacy da mantenere in vita. Questo è uno dei motivi per cui ci sono così tanti linguaggi spin-off come DDC, Cayenne, Idris, ecc.

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.