Sottotipi come sottoinsiemi di tipi di dati SML


10

Una delle poche cose che non mi piacciono del libro di Okasaki su strutture di dati puramente funzionali è che il suo codice è disseminato di un pattern inesauribile. Ad esempio, darò la sua implementazione di code in tempo reale (refactored per eliminare inutili sospensioni):

infixr 5 :::

datatype 'a stream = Nil | ::: of 'a * 'a stream lazy

structure RealTimeQueue :> QUEUE =
struct
  (* front stream, rear list, schedule stream *)
  type 'a queue = 'a stream * 'a list * 'a stream

  (* the front stream is one element shorter than the rear list *)
  fun rotate (x ::: $xs, y :: ys, zs) = x ::: $rotate (xs, ys, y ::: $zs)
    | rotate (Nil, y :: nil, zs) = y ::: $zs

  fun exec (xs, ys, _ ::: $zs) = (xs, ys, zs)
    | exec args = let val xs = rotate args in (xs, nil, xs) end

  (* public operations *)
  val empty = (Nil, nil, Nil)
  fun snoc ((xs, ys, zs), y) = exec (xs, y :: ys, zs)
  fun uncons (x ::: $xs, ys, zs) = SOME (x, exec (xs, ys, zs))
    | uncons _ = NONE
end

Come si può vedere rotatenon è esaustivo, perché non copre il caso in cui l'elenco posteriore è vuoto. La maggior parte delle implementazioni ML standard genererà un avviso al riguardo. Sappiamo che l'elenco posteriore non può essere vuoto, perché rotateil presupposto è che l'elenco posteriore abbia un elemento più lungo del flusso anteriore. Ma il controllo del tipo non lo sa - e non può assolutamente saperlo, perché questo fatto è inesprimibile nel sistema di tipi di ML.

In questo momento, la mia soluzione per sopprimere questo avviso è il seguente hack non elegante:

  fun rotate (x ::: $xs, y :: ys, zs) = x ::: $rotate (xs, ys, y ::: $zs)
    | rotate (_, ys, zs) = foldl (fn (x, xs) => x ::: $xs) zs ys

Ma quello che voglio davvero è un sistema di tipi in grado di capire che non tutte le terzine sono un valido argomento rotate. Vorrei che il sistema dei tipi mi permettesse di definire tipi come:

type 'a triplet = 'a stream * 'a list * 'a stream

subtype 'a queue of 'a triplet
  = (Nil, nil, Nil)
  | (xs, ys, zs) : 'a queue => (_ ::: $xs, _ :: ys, zs)
  | (xs, ys, zs) : 'a queue => (_ ::: $xs, ys, _ ::: $zs)

E quindi dedurre:

subtype 'a rotatable of 'a triplet
  = (xs, ys, _) : 'a rotatable => (_ ::: $xs, _ :: ys, _)
  | (Nil, y :: nil, _)

subtype 'a executable of 'a triplet
  = (xs, ys, zs) : 'a queue => (xs, ys, _ ::: $zs)
  | (xs, ys, Nil) : 'a rotatable => (xs, ys, Nil)

val rotate : 'a rotatable -> 'a stream
val exec : 'a executable -> 'a queue

Tuttavia, non voglio tipi dipendenti in piena regola, o persino GADT, o nessuna delle altre cose folli che alcuni programmatori usano. Voglio solo definire i sottotipi "scolpendo" sottoinsiemi induttivamente definiti di tipi ML esistenti. È fattibile?

Risposte:


20

Questi tipi di tipi - in cui si definisce un sottotipo (fondamentalmente) fornendo una grammatica dei valori accettabili - sono chiamati perfezionamenti del datasort .


3
L'implementazione di Rowan Davies è disponibile qui: github.com/rowandavies/sml-cidre
Noam Zeilberger

1

Posso usare GADT, TypeFamilies, DataKinds e TypeOperators (solo per l'estetica) e creare ciò che cerchi:

data Term0 varb lamb letb where
    Lam :: lamb -> Term0 varb lamb letb -> Term0 varb lamb letb
    Let :: letb -> Term0 varb lamb letb -> Term0 varb lamb letb -> Term0 varb lamb letb
    Var :: varb -> Term0 varb lamb letb
    App :: Term0 varb lamb letb -> Term0 varb lamb letb -> Term0 varb lamb letb

type Term b = Term0 b b b

data Terms = Lets | Lams | Vars

type family  t /// (ty :: Terms) where
    Term0 a b c /// Vars = Term0 Void b c
    Term0 a b c /// Lams = Term0 a Void c
    Term0 a b c /// Lets = Term0 a b Void

Now, I can write functions with more refined types:

unlet :: Term b -> Term b /// Lets

Grazie per la tua risposta. Non mi piacciono i GHC TypeFamiliesper motivi di principio: distrugge la parametricità e libera i teoremi. Inoltre non sono troppo a mio agio con i GADT, perché dato un GADT Foo a, puoi avere due tipi isomorfi Bare Qux, tali che Foo Bare Foo Quxnon sono isomorfi. Ciò contraddice l'intuizione matematica che la mappa delle funzioni è uguale a uguale - e, a livello di tipo, l'isomorfismo è la nozione giusta di uguaglianza.
pyon,

Capisco i tuoi dubbi, ma consente generalizzazioni specializzate, qualcosa che trovo abbastanza utile nella pratica.
Samuel Schlesinger,
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.