2011-03-04 12 views
31

Nel mio tempo libero sto imparando Haskell, quindi questa è una domanda per principianti.Capire come sia un'istanza di Functor

Nelle mie letture mi sono imbattuto in un esempio che illustra come Either a è fatto un'istanza di Functor:

instance Functor (Either a) where 
    fmap f (Right x) = Right (f x) 
    fmap f (Left x) = Left x 

Ora, io sto cercando di capire il motivo per cui le mappe di attuazione nel caso di un costruttore Right valore, ma non nel caso di un Left?

Qui è la mia comprensione:

Innanzitutto vorrei riscrivere l'istanza di cui sopra come

instance Functor (Either a) where 
    fmap g (Right x) = Right (g x) 
    fmap g (Left x) = Left x 

Ora:

  1. So che fmap :: (c -> d) -> f c -> f d

  2. Se sostituiamo f con Either a otteniamo fmap :: (c -> d) -> Either a c -> Either a d

  3. il tipo di Right (g x) è Either a (g x), e il tipo di g x è d, quindi abbiamo che il tipo di Right (g x) è Either a d, che è quello che ci aspettiamo da fmap (vedi 2. sopra)

  4. ora, se guardiamo Left (g x) possiamo utilizzare lo stesso ragionamento per dire che il suo tipo è Either (g x) b, cioè Either d b, che non è quello che ci aspettiamo da fmap (vedi 2. sopra): il d dovrebbe essere il secondo parametro non il primo! Quindi non possiamo mappare su Left.

Il mio ragionamento è corretto?

+11

O forse è più ovviamente un Bifunctor di un Functor - un Bifunctor ha un funzionamento bimap - bimap :: (a -> m) -> (b -> n) -> fab -> fm n. Questo ti dà la mappatura su entrambi i casi Left e Right. La libreria standard di Haskell non ha una classe Bifunctor, questo perché ci sono molti meno Bifunctor di Functors "in the wild" sebbene sia utile per Either e pair (a, b). –

+1

In 3, si usa (g x) come parte di un tipo, ma è un valore. Sembra che tu intendessi scrivere 'typof (g x)' lì, e non '(g x)' stesso. – Peaker

+0

@stephen tetley: è interessante! Grazie – MarcoS

risposta

20

Questo è giusto. C'è anche un'altra ragione abbastanza importante per questo comportamento: puoi pensare a Either a b come un calcolo, che può avere successo e restituire b o fallire con un messaggio di errore a. (Questo è anche, come funziona l'istanza monad). Quindi è naturale che l'istanza del functor non tocchi i valori Left, dal momento che si desidera mappare sul calcolo, se non riesce, non c'è nulla da manipolare.

+0

Non ho ancora imparato a conoscere le monadi :) Ho letto che "O ab" è usato nel modo in cui descrivi, e che "a" rappresenta il messaggio di errore: ma questa è solo una convenzione, giusto? – MarcoS

+0

@MarcoS: corretto, questa è solo una convenzione. O potrebbe ben essere interpretato come una disgiunzione: un valore di tipo O una b contiene un valore di tipo a o un valore di tipo b ma non entrambi. –

+0

Nel singolo caso - "Una stringa di stringa" "Una stringa di caratteri", "Una stringa di int" ecc .-- questo è così, naturalmente; non c'è differenza tra i tipi di destra e di sinistra. Ma come funtore puoi fmap over - as (o String), in quegli esempi - c'è un'asimmetria completa. Il tipo che entra nell'istanza di Functor concreta (qui 'String') deve rappresentare qualcosa di generale che può essere trovato in tutti i calcoli ordinari con qualsiasi tipo (che avviene a destra, tramite' fmap') - quindi un 'errore "L'interpretazione, sebbene difficilmente l'unica, non è un incidente. – applicative

8

Il tuo account ha ragione, ovviamente. Forse il motivo per cui abbiamo difficoltà con casi come questo è che stiamo definendo infinitamente molte istanze di functor contemporaneamente - una per ogni possibile tipo Left. Ma un'istanza di Functor è un modo sistematico di operare su infiniti tipi di sistema. Quindi stiamo definendo infiniti modi di operare sistematicamente su infiniti tipi di sistema. L'istanza implica la generalità in due modi.

Se lo fai per tappe, però, forse non è così strano.Il primo di questi tipi è una versione longwinded di Maybe utilizzando il tipo di unità () e il suo unico valore legittima ():

data MightBe b  = Nope() | Yep b 
data UnlessError b = Bad String | Good b 
data ElseInt b  = Else Int | Value b 

Qui potremmo ottenere stanchi e fare un'astrazione:

data Unless a b = Mere a  | Genuine b 

Ora facciamo le nostre istanze funtore, unproblematically, la prima guardando un po 'come l'istanza per Maybe:

instance Functor MightBe where 
    fmap f (Nope()) = Nope() -- compare with Nothing 
    fmap f (Yep x) = Yep (f x) -- compare with Just (f x) 

instance Functor UnlessError where 
    fmap f (Bad str) = Bad str -- a more informative Nothing 
    fmap f (Good x) = Good (f x) 

instance Functor ElseInt where 
    fmap f (Else n) = Else n 
    fmap f (Value b) = Value (f b) 

Ma, ancora una volta, perché preoccuparsi, facciamo l'astrazione:

instance Functor (Unless a) where 
    fmap f (Mere a) = Mere a 
    fmap f (Genuine x) = Genuine (f x) 

I Mere a termini non vengono toccati, come le (), String e Int valori non sono stati toccati.

+0

Grazie, questa è anche una spiegazione molto bella. Tuttavia, ho accettato il post di FUZxxl come risposta perché, oltre a confermare il mio ragionamento, mi dà anche un suggerimento interessante sull'interpretazione di "O a b" come computazione (monade) – MarcoS

1

Ora, sto cercando di capire perché i mappe di attuazione nel caso di un costruttore giusto valore, ma non nel caso di un sinistro?

Inserire qui e potrebbe avere senso.

Assumere a = String (un messaggio di errore) Si applica O a a Float.

Quindi hai un f: Float -> Integer dice ad esempio roundoff.

(stringa) (virgola mobile) = stringa esterna.

ora (fmap f) :: Either String Float -> Either String Int Quindi cosa hai intenzione di fare con f? f non ha idea di cosa fare con le stringhe quindi non puoi fare nulla lì. Questo è ovviamente l'unica cosa su cui puoi agire sono i valori giusti lasciando i valori di sinistra invariati.

In altre parole o A è un funtore perché c'è una FMAP così ovvia dato da:

  • per Giusti valori si applicano f
  • per i valori di sinistra fare nulla
4

Come altri hanno detto , Either type è un funtore nei suoi due argomenti. Ma in Haskell siamo in grado di (direttamente) definire solo i funtori negli ultimi argomenti di un tipo. In casi come questo, siamo in grado di aggirare la limitazione utilizzando newtype s:

newtype FlipEither b a = FlipEither { unFlipEither :: Either a b } 

Così abbiamo costruttore FlipEither :: Either a b -> FlipEither b a che avvolge un Either nelle nostre newtype con argomenti di tipo scambiati. E abbiamo il decodificatore unFlipEither :: FlipEither b a -> Either a b che lo riavvolge.Ora possiamo definire un'istanza funtore in FlipEither 'ultimo argomento s, che è in realtà Either' s primo argomento:

instance Functor (FlipEither b) where 
    fmap f (FlipEither (Left x)) = FlipEither (Left (f x)) 
    fmap f (FlipEither (Right x)) = FlipEither (Right x) 

Si noti che, se ci dimentichiamo FlipEither per un po 'si ottiene solo la definizione di Functor per Either, basta con Left/Right scambiato. E ora, ogni volta che abbiamo bisogno di un'istanza Functor nel primo argomento di tipo Either, possiamo avvolgere il valore in FlipEither e scartarlo in seguito. Per esempio:

fmapE2 :: (a -> b) -> Either a c -> Either b c 
fmapE2 f = unFlipEither . fmap f . FlipEither 

Aggiornamento: Date un'occhiata a Data.Bifunctor, di cui Either e (,) sono istanze di. Ogni bifunctor ha due argomenti ed è un functor in ciascuno di essi. Ciò si riflette nei metodi Bifunctorfirst e second.

La definizione di Bifunctor di Either è molto simmetrica:

instance Bifunctor Either where 
    bimap f _ (Left a) = Left (f a) 
    bimap _ g (Right b) = Right (g b) 

    first f = bimap f id 

    second f = bimap id f