2011-03-04 13 views
31

W wolnym czasie uczę się Haskella, więc jest to pytanie początkujące.Zrozumienie, jak Jest to instancja Functor'a

W moich odczytów natknąłem przykład ilustrujący jak Either a jest wystąpienie Functor:

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

Teraz staram się zrozumieć, dlaczego mapy wdrożeniowe w przypadku konstruktora Right wartości, ale nie w przypadku Left?

Oto moje rozumienie:

pierwsze chciałbym przepisać powyższy przypadek jako

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

now:

  1. wiem, że fmap :: (c -> d) -> f c -> f d

  2. jeśli podstawimy f z Either a otrzymujemy fmap :: (c -> d) -> Either a c -> Either a d

  3. rodzaj Right (g x) jest Either a (g x) i rodzaj g x jest d, tak mamy, że rodzaj Right (g x) jest Either a d, czyli czego oczekujemy od fmap (patrz punkt 2 powyżej)

  4. teraz, jeśli spojrzymy na Left (g x) możemy zastosować to samo rozumowanie, aby powiedzieć, że jego typ jest Either (g x) b, czyli Either d b, co nie jest, czego oczekujemy od fmap (patrz punkt 2 powyżej): the d powinien być drugi parametr nie pierwszy! Więc nie możemy mapować ponad Left.

Czy moje rozumowanie jest prawidłowe?

+11

albo być może więcej niż oczywiście Bifunctor funktora - Bifunctor ma bimap operacji - bimap :: (a -> M) -> (b -> n) -> fab -> fm n. Daje to odwzorowanie na lewą i prawą sprawę. Standardowa biblioteka Haskella nie ma klasy Bifunctor, ponieważ jest tam o wiele mniej Bifunktorów niż Funktorów "na wolności", chociaż jest przydatna zarówno dla Either, jak i pair (a, b). –

+1

W 3 używasz (g ​​x) jako części typu, ale jest to wartość. Wygląda na to, że napisałeś 'typeof (g x)' tam, a nie '(g x)' sam. – Peaker

+0

@stephen tetley: to interesujące! Dzięki – MarcoS

Odpowiedz

20

To prawda. Istnieje również inny, dość ważny powód takiego zachowania: Można pomyśleć o Either a b jako obliczeniu, które może się powieść i zwrócić b lub nie powieść się z komunikatem o błędzie a. (Jest to również sposób działania monady). Naturalne jest więc, że instancja funktora nie będzie dotykała wartości Left, ponieważ chcesz odwzorować obliczenia, jeśli się nie powiedzie, nie ma nic do manipulowania.

+0

Nie nauczyłem się jeszcze o monadach :) Czytałem, że 'Albo ab' jest używane w sposób, który opisujesz, i że' a' reprezentuje komunikat o błędzie: ale to tylko konwencja, prawda? – MarcoS

+0

@MarcoS: poprawne, to tylko konwencja. Można to interpretować jako rozróżnienie: wartość typu Albo b zawiera wartość typu a lub wartość typu b, ale nie obie. –

+0

W indywidualnym przypadku - "Albo String String" "Albo String Char", "Albo String Int" itd. - tak jest, oczywiście; nie ma różnicy pomiędzy typem prawym a lewym. Ale jako funktor możesz fmap over - jako (Either String), w tych przykładach - jest kompletna asymetria. Typ, który wchodzi w konkretną instancję Functor (tutaj 'String') musi być reprezentacją czegoś ogólnego, które można znaleźć we wszystkich zwykłych obliczeniach z dowolnego typu (co dzieje się po prawej stronie, przez' fmap') - więc "błąd" "Interpretacja, choć nie jest jedyną, nie jest przypadkiem. – applicative

8

Twoje konto ma oczywiście rację. Może powodem, dla którego mamy problemy z takimi instancjami, jest to, że definiujemy nieskończenie wiele instancji funktora na raz - jeden na każdy możliwy typ Left. Ale instancja Functor to systematyczny sposób działania na nieskończenie wielu typach w systemie. Tak więc definiujemy nieskończenie wiele sposobów systematycznego działania na nieskończenie wielu typach w systemie. Instancja obejmuje ogólność na dwa sposoby.

Jeśli jednak weźmiesz to etapami, może nie jest tak dziwnie.Pierwszy z tych typów jest rozwlekłości wersja Maybe pomocą typ jednostki () i jego jedynym uprawnionym wartość ():

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

Tutaj możemy znudzi i zrobić abstrakcję:

data Unless a b = Mere a  | Genuine b 

Teraz robimy Nasze przypadki funktora, bezproblemowy, pierwszy patrząc dużo jak na przykład dla 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) 

Ale znowu, po co się męczyć, zróbmy abstrakcję:

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

W Mere a warunki nie są dotykane, jak (), String i Int wartości nie były dotykane.

+0

Dzięki, jest to również bardzo miłe wyjaśnienie. Przyjęłam jednak stanowisko FUZxxl jako odpowiedź, ponieważ oprócz potwierdzenia mojego rozumowania, daje mi to także interesującą wskazówkę na temat interpretacji "Albo b" jako obliczenia (monada) – MarcoS

1

Teraz staram się zrozumieć, dlaczego mapy wdrożeniowe w przypadku konstruktora wartości rację, ale nie w przypadku lewicy?

Podłącz tutaj i może to mieć sens.

Założenie a = Ciąg (komunikat o błędzie) Stosuje się albo do pływaka.

Masz więc f: Float -> Integer mówi na przykład zaokrąglanie.

(Either String) (Float) = Float String.

teraz (fmap f) :: Albo String Float -> Albo String Int Więc co zrobisz z f? f nie ma pojęcia, co zrobić ze strunami, więc nie możesz tam nic zrobić. To jest oczywiście jedyną rzeczą, na którą możesz działać są prawidłowe wartości, pozostawiając niezmienione lewe wartości.

Innymi słowy Albo to funktor bo jest taka oczywista FMap dana przez:

  • dla wartości Right stosuje f
  • do wartości lewicowych zrobić nic
4

Jak inni wspomniano , Either type jest funktorem w obu argumentach. Ale w Haskell jesteśmy w stanie (bezpośrednio) zdefiniować tylko funktory w ostatnich argumentach typu. W takich przypadkach możemy obejść ograniczenia za pomocą newtype s:

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

Więc mamy konstruktora FlipEither :: Either a b -> FlipEither b a że otacza się Either do naszych newtype z zamienionych argumentów typu. I mamy dectructor unFlipEither :: FlipEither b a -> Either a b, który odwija ​​go z powrotem.Teraz możemy zdefiniować wystąpienie funktora w FlipEither „ostatni argument s, co jest rzeczywiście Either” s pierwszy argument:

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

Zauważ, że jeśli zapominamy FlipEither przez chwilę możemy uzyskać tylko definicję Functor dla Either, po prostu z zamienioną na Left/Right. A teraz, gdy potrzebujemy instancji Functor w pierwszym argumencie typu Either, możemy zawinąć tę wartość do FlipEither i rozwinąć ją później. Na przykład:

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

Aktualizacja: Zapraszamy do obejrzenia Data.Bifunctor, którego Either i (,) są instancjami. Każdy bifunctor ma dwa argumenty i jest funktorem w każdym z nich. Znajduje to odzwierciedlenie w metodach Bifunctor 's, first i second.

Definicja Bifunctor z Either bardzo symetryczne:

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