A cosa serve la funzione assurda in Data.Void?


97

La absurdfunzione in Data.Voidha la seguente firma, dove Voidè il tipo logicamente disabitato esportato da quel pacchetto:

-- | Since 'Void' values logically don't exist, this witnesses the logical
-- reasoning tool of \"ex falso quodlibet\".
absurd :: Void -> a

Conosco abbastanza logica per ottenere l'osservazione della documentazione che ciò corrisponde, dalla corrispondenza proposizioni come tipi, alla formula valida ⊥ → a.

Quello che mi lascia perplesso e curioso è: in che tipo di problemi pratici di programmazione è utile questa funzione? Penso che forse sia utile in alcuni casi come un modo sicuro per i tipi di gestire in modo esaustivo i casi "non può accadere", ma non so abbastanza sugli usi pratici di Curry-Howard per dire se quell'idea è sul traccia giusta a tutti.

EDIT: esempi preferibilmente in Haskell, ma se qualcuno vuole usare un linguaggio tipizzato in modo dipendente non mi lamenterò ...


5
Una rapida ricerca mostra che la absurdfunzione è stata utilizzata in questo articolo che tratta della Contmonade: haskellforall.com/2012/12/the-continuation-monad.html
Artyom

6
Puoi vedere absurdcome una direzione dell'isomorfismo tra Voide forall a. a.
Daniel Wagner

Risposte:


61

La vita è un po 'dura, poiché Haskell non è severa. Il caso d'uso generale è quello di gestire percorsi impossibili. Per esempio

simple :: Either Void a -> a
simple (Left x) = absurd x
simple (Right y) = y

Questo risulta essere in qualche modo utile. Considera un tipo semplice perPipes

data Pipe a b r
  = Pure r
  | Await (a -> Pipe a b r)
  | Yield !b (Pipe a b r)

questa è una versione rigorosa e semplificata del tipo pipe standard dalla Pipeslibreria di Gabriel Gonzales . Ora, possiamo codificare una pipe che non restituisce mai (cioè un consumatore) come

type Consumer a r = Pipe a Void r

questo davvero non cede mai. L'implicazione di ciò è che la regola di piegatura corretta per a Consumerè

foldConsumer :: (r -> s) -> ((a -> s) -> s) -> Consumer a r -> s
foldConsumer onPure onAwait p 
 = case p of
     Pure x -> onPure x
     Await f -> onAwait $ \x -> foldConsumer onPure onAwait (f x)
     Yield x _ -> absurd x

o in alternativa, che puoi ignorare il caso del rendimento quando hai a che fare con i consumatori. Questa è la versione generale di questo modello di progettazione: usa tipi di dati polimorfici e Voidper eliminare le possibilità quando è necessario.

Probabilmente l'uso più classico di Voidè in CPS.

type Continuation a = a -> Void

cioè, a Continuationè una funzione che non ritorna mai. Continuationè la versione tipo di "non". Da questo otteniamo una monade di CPS (corrispondente alla logica classica)

newtype CPS a = Continuation (Continuation a)

poiché Haskell è puro, non possiamo ottenere nulla di questo tipo.


1
Eh, posso davvero seguire quel bit di CPS. Certamente avevo già sentito parlare della doppia negazione Curry-Howard / CPS prima, ma non l'avevo capito; Non ho intenzione di affermare di averlo capito completamente ora, ma questo sicuramente aiuta!
Luis Casillas

"La vita è un po 'dura, dal momento che Haskell non è severo " - cosa intendevi esattamente con questo?
Erik Kaplun

5
@ErikAllik, in un linguaggio rigoroso, Voidè disabitato. In Haskell, contiene _|_. In un linguaggio rigoroso, un costruttore di dati che accetta un argomento di tipo Voidnon può mai essere applicato, quindi il lato destro della corrispondenza del modello è irraggiungibile. In Haskell, è necessario utilizzare a !per imporlo, e GHC probabilmente non noterà che il percorso è irraggiungibile.
dfeuer

che ne dici di Agda? è pigro ma ce l'ha _|_? e allora soffre della stessa limitazione?
Erik Kaplun

1
agda è, in generale, totale e quindi l'ordine di valutazione non è osservabile. Non esiste un termine agda chiuso del tipo vuoto a meno che non si disattivi il controllo di terminazione o qualcosa del genere
Philip JF

58

Considera questa rappresentazione per termini lambda parametrizzati dalle loro variabili libere. (Vedi articoli di Bellegarde e Hook 1994, Bird e Paterson 1999, Altenkirch e Reus 1999.)

data Tm a  = Var a
           | Tm a :$ Tm a
           | Lam (Tm (Maybe a))

Puoi certamente renderlo un Functor, catturare il concetto di rinomina e Monadcatturare il concetto di sostituzione.

instance Functor Tm where
  fmap rho (Var a)   = Var (rho a)
  fmap rho (f :$ s)  = fmap rho f :$ fmap rho s
  fmap rho (Lam t)   = Lam (fmap (fmap rho) t)

instance Monad Tm where
  return = Var
  Var a     >>= sig  = sig a
  (f :$ s)  >>= sig  = (f >>= sig) :$ (s >>= sig)
  Lam t     >>= sig  = Lam (t >>= maybe (Var Nothing) (fmap Just . sig))

Consideriamo ora i termini chiusi : questi sono gli abitanti di Tm Void. Dovresti essere in grado di incorporare i termini chiusi in termini con variabili libere arbitrarie. Come?

fmap absurd :: Tm Void -> Tm a

Il problema, ovviamente, è che questa funzione attraverserà il termine senza fare proprio nulla. Ma è un tocco più onesto di unsafeCoerce. Ed è per questo che è vacuousstato aggiunto a Data.Void...

Oppure scrivi un valutatore. Ecco i valori con variabili libere in b.

data Val b
  =  b :$$ [Val b]                              -- a stuck application
  |  forall a. LV (a -> Val b) (Tm (Maybe a))   -- we have an incomplete environment

Ho appena rappresentato i lambda come chiusure. Il valutatore è parametrizzato da un ambiente che associa le variabili libere aai valori superiori b.

eval :: (a -> Val b) -> Tm a -> Val b
eval g (Var a)   = g a
eval g (f :$ s)  = eval g f $$ eval g s where
  (b :$$ vs)  $$ v  = b :$$ (vs ++ [v])         -- stuck application gets longer
  LV g t      $$ v  = eval (maybe v g) t        -- an applied lambda gets unstuck
eval g (Lam t)   = LV g t

Hai indovinato. Per valutare un termine chiuso a qualsiasi target

eval absurd :: Tm Void -> Val b

Più in generale, Voidè usato raramente da solo, ma è utile quando si desidera istanziare un parametro di tipo in un modo che indica una sorta di impossibilità (ad esempio, qui, utilizzando una variabile libera in un termine chiuso). Spesso questi tipi parametrizzati vengono con funzioni di ordine superiore operazioni di sollevamento sui parametri per operazioni del tipo complesso (ad esempio, qui, fmap, >>=, eval). Quindi passi absurdcome operazione generica Void.

Per un altro esempio, immagina di usare Either e vper acquisire calcoli che si spera ti diano un vma potrebbero sollevare un'eccezione di tipo e. È possibile utilizzare questo approccio per documentare in modo uniforme il rischio di comportamenti scorretti. Per calcoli perfettamente funzionanti in questa impostazione, prendi eper essere Void, quindi usa

either absurd id :: Either Void v -> v

per correre in sicurezza o

either absurd Right :: Either Void v -> Either e v

per incorporare componenti sicuri in un mondo non sicuro.

Oh, e un ultimo evviva, gestire un "non può succedere". Si presenta nella costruzione generica della cerniera, ovunque il cursore non possa essere.

class Differentiable f where
  type D f :: * -> *              -- an f with a hole
  plug :: (D f x, x) -> f x       -- plugging a child in the hole

newtype K a     x  = K a          -- no children, just a label
newtype I       x  = I x          -- one child
data (f :+: g)  x  = L (f x)      -- choice
                   | R (g x)
data (f :*: g)  x  = f x :&: g x  -- pairing

instance Differentiable (K a) where
  type D (K a) = K Void           -- no children, so no way to make a hole
  plug (K v, x) = absurd v        -- can't reinvent the label, so deny the hole!

Ho deciso di non cancellare il resto, anche se non è esattamente rilevante.

instance Differentiable I where
  type D I = K ()
  plug (K (), x) = I x

instance (Differentiable f, Differentiable g) => Differentiable (f :+: g) where
  type D (f :+: g) = D f :+: D g
  plug (L df, x) = L (plug (df, x))
  plug (R dg, x) = R (plug (dg, x))

instance (Differentiable f, Differentiable g) => Differentiable (f :*: g) where
  type D (f :*: g) = (D f :*: g) :+: (f :*: D g)
  plug (L (df :&: g), x) = plug (df, x) :&: g
  plug (R (f :&: dg), x) = f :&: plug (dg, x)

In realtà, forse è rilevante. Se ti senti avventuroso, questo articolo incompleto mostra come utilizzare Voidper comprimere la rappresentazione dei termini con variabili libere

data Term f x = Var x | Con (f (Term f x))   -- the Free monad, yet again

in qualsiasi sintassi generata liberamente da a Differentiablee Traversablefuntore f. Usiamo Term f Voidper rappresentare regioni senza variabili libere e [D f (Term f Void)]per rappresentare tubi che scorrono attraverso regioni senza variabili libere o su una variabile libera isolata o su una giunzione nei percorsi di due o più variabili libere. Devo finire quell'articolo prima o poi.

Per un tipo senza valori (o almeno, nessuno di cui valga la pena parlare in gentile compagnia), Voidè straordinariamente utile. Ed absurdè come lo usi.


Sarebbe forall f. vacuous f = unsafeCoerce funa regola di riscrittura GHC valida?
Cactus

1
@ Cactus, non proprio. Le Functoristanze fasulle potrebbero essere GADT che in realtà non sono niente come funtori.
dfeuer

Quelle Functornon infrangerebbero la fmap id = idregola? O è questo che intendi per "fasullo" qui?
Cactus

35

Penso che forse è utile in alcuni casi come un modo sicuro per i tipi di gestire in modo esaustivo i casi "non può accadere"

È proprio così.

Si potrebbe dire che absurdnon è più utile di const (error "Impossible"). Tuttavia, è limitato al tipo, in modo che il suo unico input possa essere qualcosa di tipo Void, un tipo di dati che viene lasciato intenzionalmente disabitato. Ciò significa che non esiste un valore effettivo a cui puoi passare absurd. Se ti capita mai di finire in un ramo di codice in cui il controllo del tipo pensa che tu abbia accesso a qualcosa di tipo Void, allora, beh, sei in una situazione assurda . Quindi usi solo absurdper contrassegnare fondamentalmente che questo ramo di codice non dovrebbe mai essere raggiunto.

"Ex falso quodlibet" significa letteralmente "da [a] falsa [proposizione], qualunque cosa segue". Quindi, quando scopri di avere in mano un dato di cui è il tipo Void, sai di avere false prove nelle tue mani. Puoi quindi riempire qualsiasi buco che desideri (tramite absurd), perché da una falsa proposizione, segue qualsiasi cosa.

Ho scritto un post sul blog sulle idee alla base di Conduit che ha un esempio di utilizzo absurd.

http://unknownparallel.wordpress.com/2012/07/30/pipes-to-conduits-part-6-leftovers/#running-a-pipeline


13

In generale, puoi usarlo per evitare corrispondenze di pattern apparentemente parziali. Ad esempio, ricavando un'approssimazione delle dichiarazioni del tipo di dati da questa risposta :

data RuleSet a            = Known !a | Unknown String
data GoRuleChoices        = Japanese | Chinese
type LinesOfActionChoices = Void
type GoRuleSet            = RuleSet GoRuleChoices
type LinesOfActionRuleSet = RuleSet LinesOfActionChoices

Quindi potresti usare in absurdquesto modo, ad esempio:

handleLOARules :: (String -> a) -> LinesOfActionsRuleSet -> a
handleLOARules f r = case r of
    Known   a -> absurd a
    Unknown s -> f s

13

Esistono diversi modi per rappresentare il tipo di dati vuoto . Uno è un tipo di dati algebrico vuoto. Un altro modo è renderlo un alias per ∀α.α o

type Void' = forall a . a

in Haskell - questo è il modo in cui possiamo codificarlo nel Sistema F (vedere il Capitolo 11 di Prove e tipi ). Queste due descrizioni sono ovviamente isomorfiche e l'isomorfismo è testimoniato di tanto \x -> x :: (forall a.a) -> Voidin tanto absurd :: Void -> a.

In alcuni casi, preferiamo la variante esplicita, di solito se il tipo di dati vuoto appare in un argomento di una funzione, o in un tipo di dati più complesso, come in Data.Conduit :

type Sink i m r = Pipe i i Void () m r

In alcuni casi, preferiamo la variante polimorfica, di solito il tipo di dati vuoto è coinvolto nel tipo di ritorno di una funzione.

absurd nasce quando stiamo convertendo tra queste due rappresentazioni.


Ad esempio, callcc :: ((a -> m b) -> m a) -> m autilizza (implicito) forall b. Potrebbe essere anche di tipo ((a -> m Void) -> m a) -> m a, perché una chiamata alla continazione non ritorna effettivamente, trasferisce il controllo a un altro punto. Se volessimo lavorare con continuazioni, potremmo definire

type Continuation r a = a -> Cont r Void

(Potremmo usare, type Continuation' r a = forall b . a -> Cont r bma ciò richiederebbe i tipi di rango 2.) E poi, vacuousMconverte questo Cont r Voidin Cont r b.

(Si noti inoltre che è possibile utilizzare haskellers.com per cercare l'utilizzo (dipendenze inverse) di un determinato pacchetto, ad esempio per vedere chi e come utilizza il pacchetto void .)


TypeApplicationspuò essere utilizzato per essere più espliciti sui dettagli di proof :: (forall a. a) -> Void: proof fls = fls @Void.
Iceland_jack

1

In linguaggi tipizzati in modo dipendente come Idris, è probabilmente più utile che in Haskell. In genere, in una funzione totale, quando si esegue la corrispondenza del modello a un valore che in realtà non può essere inserito nella funzione, si costruisce quindi un valore di tipo disabitato e si utilizza absurdper finalizzare la definizione del caso.

Ad esempio, questa funzione rimuove un elemento da un elenco con il vincolo a livello di tipo che è presente lì:

shrink : (xs : Vect (S n) a) -> Elem x xs -> Vect n a
shrink (x :: ys) Here = ys
shrink (y :: []) (There p) = absurd p
shrink (y :: (x :: xs)) (There p) = y :: shrink (x :: xs) p

Dove il secondo caso sta dicendo che c'è un certo elemento in una lista vuota, il che è, beh, assurdo. In generale, tuttavia, il compilatore non lo sa e spesso dobbiamo essere espliciti. Quindi il compilatore può verificare che la definizione della funzione non sia parziale e si ottengono maggiori garanzie in fase di compilazione.

Dal punto di vista di Curry-Howard, dove sono le proposizioni, allora absurdè una sorta di QED in una dimostrazione per contraddizione.

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.