Chiarimento sui tipi esistenziali in Haskell


10

Sto cercando di capire i tipi esistenziali in Haskell e ho trovato un PDF http://www.ii.uni.wroc.pl/~dabi/courses/ZPF15/rlasocha/prezentacja.pdf

Per favore, correggi le mie intese che ho fino ad ora.

  • I tipi esistenziali non sembrano essere interessati al tipo che contengono, ma i pattern corrispondenti ad essi dicono che esiste un tipo che non conosciamo fino a quando non usiamo Typeable o Data.
  • Li usiamo quando vogliamo nascondere i tipi (es: per gli elenchi eterogenei) o non sappiamo davvero quali siano i tipi al momento della compilazione.
  • GADT's forniscono la sintassi chiara e meglio codice utilizzando tipi esistenziali, fornendo impliciti forall' s

I miei dubbi

  • Nella pagina 20 del PDF sopra è indicato per il codice sottostante che è impossibile per una funzione richiedere un buffer specifico. Perché è così? Quando sto redigendo una funzione, so esattamente che tipo di buffer userò anche se potrei non sapere quali dati inserirò in quello. Cosa c'è di sbagliato nell'avere :: Worker MemoryBuffer IntSe vogliono veramente astrarre su Buffer possono avere un tipo Sum data Buffer = MemoryBuffer | NetBuffer | RandomBuffere avere un tipo simile:: Worker Buffer Int
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer

memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int
  • Dato che Haskell è un linguaggio di cancellazione di tipo completo come C, come fa a sapere a Runtime quale funzione chiamare. È come se avessimo poche informazioni e passassimo in una V-Table of Functions enorme e in fase di esecuzione capirà da V-Table? Se è così, che tipo di informazioni memorizzerà?

Risposte:


8

GADT fornisce la sintassi chiara e migliore per codificare usando i tipi esistenziali fornendo forall impliciti

Penso che ci sia un accordo generale sul fatto che la sintassi di GADT sia migliore. Non direi che è perché i GADT forniscono forks impliciti, ma piuttosto perché la sintassi originale, abilitata con l' ExistentialQuantificationestensione, è potenzialmente confusa / fuorviante. Tale sintassi, ovviamente, assomiglia a:

data SomeType = forall a. SomeType a

o con un vincolo:

data SomeShowableType = forall a. Show a => SomeShowableType a

e penso che il consenso sia che l'uso della parola chiave forallqui permetta al tipo di essere facilmente confuso con il tipo completamente diverso:

data AnyType = AnyType (forall a. a)    -- need RankNTypes extension

Una sintassi migliore avrebbe potuto usare una existsparola chiave separata , quindi dovresti scrivere:

data SomeType = SomeType (exists a. a)   -- not valid GHC syntax

La sintassi GADT, usata con implicito o esplicito forall, è più uniforme tra questi tipi e sembra essere più facile da capire. Anche con un esplicito forall, la seguente definizione porta l'idea che puoi prendere un valore di qualsiasi tipo ae inserirlo in un monomorfo SomeType':

data SomeType' where
    SomeType' :: forall a. (a -> SomeType')   -- parentheses optional

ed è facile vedere e comprendere la differenza tra quel tipo e:

data AnyType' where
    AnyType' :: (forall a. a) -> AnyType'

I tipi esistenziali non sembrano essere interessati al tipo che contengono, ma i pattern corrispondenti ad essi dicono che esiste un tipo che non conosciamo fino a quando non usiamo Typeable o Data.

Li usiamo quando vogliamo nascondere i tipi (es: per gli elenchi eterogenei) o non sappiamo davvero quali siano i tipi al momento della compilazione.

Immagino che questi non siano troppo lontani, anche se non devi usare Typeableo Datausare tipi esistenziali. Penso che sarebbe più preciso dire che un tipo esistenziale fornisce una "scatola" ben tipizzata attorno a un tipo non specificato. La casella "nasconde" il tipo in un certo senso, che consente di creare un elenco eterogeneo di tali caselle, ignorando i tipi che contengono. Si scopre che un esistenziale non vincolato, come SomeType'sopra, è piuttosto inutile, ma un tipo vincolato:

data SomeShowableType' where
    SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'

ti permette di modellare la corrispondenza per sbirciare all'interno della "scatola" e rendere disponibili le strutture di tipo classe:

showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x

Nota che questo funziona per qualsiasi classe di tipo, non solo Typeableo Data.

Per quanto riguarda la tua confusione sulla pagina 20 del deck diapositive, l'autore sta dicendo che è impossibile per una funzione che richiede un esistenziale Worker richiedere un'istanza Workerparticolare Buffer. Puoi scrivere una funzione per crearne una Workerusando un tipo particolare di Buffer, come MemoryBuffer:

class Buffer b where
  output :: String -> b -> IO ()
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer
instance Buffer MemoryBuffer

memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int

ma se scrivi una funzione che accetta un Workerargomento come, può usare solo le Bufferstrutture di classe di tipo generale (ad es. la funzione output):

doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b

Non può provare a richiedere che si btratti di un particolare tipo di buffer, anche tramite la corrispondenza dei modelli:

doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
  MemoryBuffer -> error "try this"       -- type error
  _            -> error "try that"

Infine, le informazioni di runtime sui tipi esistenziali sono rese disponibili attraverso argomenti impliciti "dizionario" per le typeclass coinvolte. Il Workertipo sopra, oltre ad avere campi per il buffer e l'input, ha anche un campo implicito invisibile che punta al Bufferdizionario (un po 'come v-table, anche se non è enorme, poiché contiene solo un puntatore alla outputfunzione appropriata ).

Internamente, la classe type Bufferè rappresentata come un tipo di dati con campi funzione e le istanze sono "dizionari" di questo tipo:

data Buffer' b = Buffer' { output' :: String -> b -> IO () }

dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }

Il tipo esistenziale ha un campo nascosto per questo dizionario:

data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }

e una funzione come doWorkquella opera su Worker'valori esistenziali è implementata come:

doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b

Per una classe di tipo con una sola funzione, il dizionario è effettivamente ottimizzato su un nuovo tipo, quindi in questo esempio il Workertipo esistenziale include un campo nascosto che consiste in un puntatore alla outputfunzione per il buffer, e questa è l'unica informazione di runtime necessaria di doWork.


Le esistenze sono come il Rango 1 per le dichiarazioni dei dati? Le esistenze sono il modo di gestire le funzioni virtuali in Haskell come in qualsiasi linguaggio OOP?
Pawan Kumar,

1
Probabilmente non avrei dovuto chiamare AnyTypeun tipo di livello 2; è solo confuso e l'ho eliminato. Il costruttore si AnyTypecomporta come una funzione di grado 2 e il costruttore SomeTypesvolge una funzione di grado 1 (proprio come la maggior parte dei tipi inesistenti), ma questa non è una caratterizzazione molto utile. Semmai, ciò che rende interessanti questi tipi è che sono al loro posto 0 (cioè, non quantificati su una variabile di tipo e quindi monomorfi) anche se "contengono" tipi quantificati.
KA Buhr,

1
Le classi di tipi (e in particolare le loro funzioni di metodo) piuttosto che i tipi esistenziali, sono probabilmente l'equivalente di Haskell più diretto rispetto alle funzioni virtuali. In senso tecnico, le classi e gli oggetti dei linguaggi OOP possono essere visti come tipi e valori esistenziali, ma praticamente ci sono spesso modi migliori per implementare lo stile del polimorfismo OOP "funzione virtuale" di Haskell rispetto agli esistenziali, come i tipi di somma, classi di tipo e / o polimorfismo parametrico.
KA Buhr

4

Nella pagina 20 del PDF sopra è indicato per il codice sottostante che è impossibile per una funzione richiedere un buffer specifico. Perché è così?

Perché Worker, come definito, accetta solo un argomento, il tipo del campo "input" (variabile di tipo x). Ad esempio Worker Intè un tipo. La variabile type b, invece, non è un parametro di Worker, ma è una sorta di "variabile locale", per così dire. Non può essere passato come in Worker Int String- ciò causerebbe un errore di tipo.

Se invece abbiamo definito:

data Worker x b = Worker {buffer :: b, input :: x}

allora Worker Int Stringfunzionerebbe, ma il tipo non è più esistenziale - ora dobbiamo sempre passare anche il tipo di buffer.

Dato che Haskell è un linguaggio di cancellazione di tipo completo come C, come fa a sapere a Runtime quale funzione chiamare. È come se avessimo poche informazioni e passassimo in una V-Table of Functions enorme e in fase di esecuzione capirà da V-Table? Se è così, che tipo di informazioni memorizzerà?

Questo è approssimativamente corretto. In breve, ogni volta che si applica il costruttore Worker, GHC deduce il btipo dagli argomenti di Worker, quindi cerca un'istanza Buffer b. Se viene trovato, GHC include un puntatore aggiuntivo all'istanza nell'oggetto. Nella sua forma più semplice, questo non è troppo diverso dal "puntatore a vtable" che viene aggiunto a ciascun oggetto in OOP quando sono presenti funzioni virtuali.

In generale, tuttavia, può essere molto più complesso. Il compilatore potrebbe utilizzare una rappresentazione diversa e aggiungere più puntatori anziché uno singolo (ad esempio, aggiungendo direttamente i puntatori a tutti i metodi di istanza), se ciò accelera il codice. Inoltre, a volte il compilatore deve utilizzare più istanze per soddisfare un vincolo. Ad esempio, se abbiamo bisogno di memorizzare l'istanza per Eq [Int]... allora non ci sono uno ma due: uno per Inte uno per gli elenchi, e i due devono essere combinati (in fase di esecuzione, salvo le ottimizzazioni).

È difficile indovinare esattamente cosa fa GHC in ogni caso: ciò dipende da una tonnellata di ottimizzazioni che potrebbero o meno innescarsi.

Puoi provare a cercare su Google l'implementazione "classiforme" delle classi di tipi per vedere di più su cosa sta succedendo. Puoi anche chiedere a GHC di stampare il Core ottimizzato interno con -ddump-simple osservare i dizionari in fase di costruzione, memorizzazione e passaggio. Devo avvertirti: Core è piuttosto basso livello e all'inizio può essere difficile da leggere.

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.