2015-09-22 18 views
10

In "Tipi di dati alla carte" Swierstra scrive che dato Free (che egli chiama Term) e Zero è possibile implementare la monade Identità:Scrivendo la monade identità in termini di libera

data Term f a = Pure a 
       | Impure (f (Term f a)) 
data Zero a 

Term Zero è ora il Identità monade Capisco perché questo è. Il problema è che non riesco mai a usare Term Zero come Monade a causa del fastidioso Functor f => vincolo:

instance Functor f => Monad (Term f) where 
    return x = Pure x 
    (Pure x) >>= f = f x 
    (Impure f) >>= t = Impure (fmap (>>=f) t) 

Come faccio Zero un funtore?

instance Functor Zero where 
    fmap f z = ??? 

Sembra che ci sia un trucco qui: Dal momento che non ci sono Zero costruttori, Impure non può essere utilizzato, e così il Impure caso di >>= non viene mai chiamato. Questo significa fmap non è mai chiamato, quindi non c'è un senso in cui questo è ok:

instance Functor Zero where 
    fmap f z = undefined 

Il problema è, questo si sente come barare. Cosa mi manca? Zero è in realtà un Functional? O forse Zero non è un Functor, e questo è un difetto di come esprimiamo Free in Haskell?

+1

sì, 'Zero' è un funtore da' Hask' a '0', la categoria vuota (che poi è incorporato di nuovo in Hask). –

risposta

12

Se si accende DeriveFunctor, è possibile scrivere

data Zero a deriving Functor 

ma si potrebbe considerare che barare. Se volete scrivere voi stessi, è possibile attivare EmptyCase, invece, quindi scrivere il

instance Functor Zero where 
    fmap f z = case z of 
+0

Mi chiedevo cosa succederebbe se imbrogliassimo e facessimo 'fmap f _ | _' in entrambe le istanze di questi functor. In GHCi 7.10.2, la prima istanza errori con 'Exception: Void fmap', senza provare a valutare il fondo, mentre il secondo lo valuta. Mi sarei aspettato invece un errore non esaustivo. Non che ciò sia davvero importante - entrambe le istanze vanno bene, anche se producono un fondo diverso se ne passiamo una. – chi

+2

@chi Sicuramente non dovresti aspettarti un errore di non esaustività: questi pattern match sono esaurienti! –

+0

Mi aspettavo in qualche modo l'equivalenza pratica tra 'case e di p1 -> e1; ..; pn -> en' e 'caso e di ...; pn -> en; _ -> errore "non esaustivo" '. Questa equivalenza "sotto il cofano" si rompe solo per n = 0 (se non identifichiamo i fondi "diversi"). A proposito di esaustività ... Sarei più convinto in Agda/Coq o in qualsiasi altro linguaggio di programmazione totale. In Haskell hai ragione, ma i fondi sono bestie strane, e non ho la chiara idea se 'case _ | _ :: Void of {}' valuti o meno l'argomento, operativamente parlando. – chi

7

Un modo funky-cercando di andare su questo è quello di utilizzare Data.Void invece di un data dichiarazione di vuoto, in questo modo:

import Data.Void 

-- `Zero a` is isomorphic to `Void` 
newtype Zero a = Zero Void 

instance Functor Zero where 
    -- You can promise anything if the precondition is that 
    -- somebody's gotta give you an `x :: Void`. 
    fmap f (Zero x) = absurd x 

Vedi this question per alcuni davvero grande spiegazioni di ciò che è utile per Void. Ma l'idea chiave è che la funzione absurd :: Void -> a ti consente di passare dall'impossibilità x :: Void a qualsiasi tipo che ti piace.

7

Un modo intelligente per definire Zero a è il seguente.

newtype Zero a = Zero (Zero a) 

In altre parole, si codifica il tipo di sciocco, dichiarazione un po 'non-ovvio che v'è in realtà un modo per ottenere un valore di Zero a: è necessario avere già uno!

Con questa definizione, absurd :: Zero a -> b è definibile in modo "naturale"

absurd :: Zero a -> b 
absurd (Zero z) = absurd z 

In un certo senso queste definizioni sono tutti legali perché sottolineano esattamente come non sono possibili istanziare. Nessun valore di Zero a può essere costruito a meno che qualcun altro non "imbrogli" per primo.

instance Functor Zero where 
    fmap f = absurd 
+0

Vale la pena sottolineare che questo è più o meno come viene implementato il modulo 'Data.Void'. –

+0

Hah, sì, è vero! –

+1

'Data.Void' faceva questo, prima che uscisse' EmptyCase' (era cambiato quando si spostava da 'void' a' base'). Non so se è cambiato per motivi di chiarezza, efficienza o segnalazione degli errori. Fai? – dfeuer

3

Un altro giro su Luis Casillas di answer è quello di costruire il vostro tipo del tutto a magazzino parti:

type Id = Free (Const Void) 

L'istanza Functor per Const x funzionerà come si desidera (la sua fmap non fa nulla, che va bene), e avrete solo bisogno di prendersi cura durante il disimballaggio:

runId (Pure x) = x 
runId (Free (Const ab)) = absurd ab 

Tutte queste cose, di Ovviamente, sono solo "moralmente" equivalenti a Identity, perché tutti introducono valori extra. In particolare, siamo in grado di distinguere tra

bottom 
Pure bottom 
Free bottom