2012-11-26 12 views
15

Staram się zrozumieć, dlaczego te dwa fragmenty dają różne wyniki w ramach tak zwanej "analizy ścisłości człowieka ubogiego".Lenistwo/surowość między danymi a nowym typem

Pierwszy przykład wykorzystuje data (zakładając właściwe wystąpienie aplikacyjny)

data Parser t a = Parser { 
     getParser :: [t] -> Maybe ([t], a) 
    } 

> getParser (pure (,) <*> literal ';' <*> undefined) "abc" 
*** Exception: Prelude.undefined 

Drugi wykorzystuje newtype. Nie ma innej różnicy:

newtype Parser t a = Parser { 
     getParser :: [t] -> Maybe ([t], a) 
    } 

> getParser (pure (,) <*> literal ';' <*> undefined) "abc" 
Nothing 

literal x jest parser że powiedzie zużywa jeden znak wejścia, jeśli jego argumentem mecze pierwszego tokena. W tym przykładzie kończy się niepowodzeniem, ponieważ ; nie pasuje do a. Jednak w przykładzie data nadal widać, że następny parser jest niezdefiniowany, podczas gdy przykład newtype nie.

Przeczytałem this, this i this, ale nie rozumiem ich wystarczająco dobrze, aby dowiedzieć się, dlaczego pierwszy przykład jest niezdefiniowany. Wydaje mi się, że w tym przykładzie newtype jest leniwy niż data, przeciwieństwo tego, co mówią odpowiedzi. (Przynajmniej przez to też zdezorientowało to one other person).

Dlaczego zmiana z data na newtype zmienia definicję tego przykładu?


Oto kolejna rzecz odkryłem: z tym aplikacyjnych przykład data parser powyżej wyjść niezdefiniowany:

instance Applicative (Parser s) where 
    Parser f <*> Parser x = Parser h 
    where 
     h xs = 
     f xs >>= \(ys, f') -> 
     x ys >>= \(zs, x') -> 
     Just (zs, f' x') 

    pure a = Parser (\xs -> Just (xs, a)) 

natomiast w tym przypadku data parser powyżej robi nie wyjście niezdefiniowany (zakładając, poprawna instancja Monada dla Parser s):

instance Applicative (Parser s) where 
    f <*> x = 
     f >>= \f' -> 
     x >>= \x' -> 
     pure (f' x') 

    pure = pure a = Parser (\xs -> Just (xs, a)) 

Pełna fragment kodu:

import Control.Applicative 
import Control.Monad (liftM) 

data Parser t a = Parser { 
     getParser :: [t] -> Maybe ([t], a) 
    } 


instance Functor (Parser s) where 
    fmap = liftM 

instance Applicative (Parser s) where 
    Parser f <*> Parser x = Parser h 
    where 
     h xs = f xs >>= \(ys, f') -> 
     x ys >>= \(zs, x') -> 
     Just (zs, f' x') 

    pure = return 


instance Monad (Parser s) where 
    Parser m >>= f = Parser h 
    where 
     h xs = 
      m xs >>= \(ys,y) -> 
      getParser (f y) ys 

    return a = Parser (\xs -> Just (xs, a)) 


literal :: Eq t => t -> Parser t t 
literal x = Parser f 
    where 
    f (y:ys) 
     | x == y = Just (ys, x) 
     | otherwise = Nothing 
    f [] = Nothing 
+2

Kiedy zadajesz takie pytanie, na ogół lepiej jest dołączyć cały odpowiedni kod, jeśli jest wystarczająco mały, aby zmieścić (dotyczy to instancji 'Functor' i' Monad' oraz 'literału'), tak aby ludzie nie musisz dokładnie zgadywać, w jaki sposób pisałeś funkcje (jak już zauważyłeś, nawet niewielkie zmiany mogą mieć wpływ na zachowanie). – shachaf

+1

@ shachaf prawdziwe pytanie tutaj nie jest "jak mogę naprawić mój kod?" - Już to zrobiłem - ale "czym różni się" data "od" newtype "od surowości/lenistwa?" Przepraszam, jeśli to nie wynika z pytania. –

+0

Tak, ale mimo to, jak możemy wyjaśnić, co się dzieje z Twoim kodem, nie wiedząc, jak wygląda kod? – shachaf

Odpowiedz

18

Jak zapewne wiecie, główną różnicą między data i newtype jest to, że z data że data konstruktorzy są leniwi podczas newtype jest ścisły, czyli biorąc pod uwagę następujące rodzaje

data D a = D a 
newtype N a = N a 

następnie D ⊥ `seq` x = x, ale N ⊥ `seq` x = ⊥.

Co jest może mniej znany jest jednak to, że podczas spotkania na wzór tych konstruktorów, role są „odwrócone”, to znaczy z

constD x (D y) = x 
constN x (N y) = x 

następnie constD x ⊥ = ⊥, ale constN x ⊥ = x.

Oto, co dzieje się w twoim przykładzie.

Parser f <*> Parser x = Parser h where ... 

Z data, mecz wzór w definicji <*> będzie odbiegać od razu, czy też argumenty są , ale newtype konstruktorzy są ignorowane i jest jak gdybyś napisał

f <*> x = h where 

, które będą rozbieżne tylko dla x = ⊥, jeśli wymagane jest x.

+0

Dwie rzeczy, które wciąż nie są do końca jasne, to: 1) czy różnica dopasowania wzoru jest konieczna z powodu semantyki Haskella i 2) czy różnica dopasowania wzoru wynika z różnicy dokładności konstruktora. –

+0

@MattFenwick: 1) Tak, ponieważ newtypes w zasadzie nie istnieje semantycznie, dopasowywanie wzorca na jednym jest takie samo jak dopasowywanie do wzorca na typie podstawowym, więc ponieważ wzorzec 'x' nie ocenia' x ', ani wzór 'N x'. 2) Nie. Rozważ "dane S a = S! A; constS x (S y) = x', następnie '' 'S undefined' seq' x = ⊥''' oraz 'constS x ⊥ = ⊥'. – hammar

+0

Tak więc w przypadku konstruktora danych kompilator musi ocenić wystarczająco daleko, aby określić, czy konstruktor pasuje do wzorca? –

10

Różnica data i newtype jest data jest „podnoszone” i newtype nie. Oznacza to, że data ma dodatkowe ⊥ - w tym przypadku oznacza to, że undefined/= Parser undefined. Kiedy twój wzór Applicative - pasuje do Parser x, wymusza wartość , jeśli konstruktor.

Po dopasowaniu do wzoru na konstruktorze data, jest on oceniany i rozbierany, aby upewnić się, że nie jest ⊥. Na przykład:

λ> data Foo = Foo Int deriving Show 
λ> case undefined of Foo _ -> True 
*** Exception: Prelude.undefined 

Więc dopasowywania wzorca na data konstruktora jest surowe, i będzie go zmuszać. Z drugiej strony, newtype jest reprezentowany w dokładnie taki sam sposób, jak typ, który owija jego konstruktor. Tak dopasowane na newtype konstruktora robi absolutnie nic:

λ> newtype Foo = Foo Int deriving Show 
λ> case undefined of Foo _ -> True 
True 

Istnieją prawdopodobnie dwa sposoby, aby zmienić swój program data taki sposób, że nie psuje. Jedną z nich jest użycie niezaprzeczalnego dopasowania wzorców w instancji Applicative, która zawsze "powiedzie się" (ale użycie dopasowanych wartości w dowolnym miejscu później może się nie udać). Każdy mecz newtype zachowuje się jak niezaprzeczalny wzorzec (ponieważ nie ma konstruktora, który mógłby dopasować, zgodnie ze ścisłością).

λ> data Foo = Foo Int deriving Show 
λ> case undefined of ~(Foo _) -> True 
True 

Drugi byłoby użyć Parser undefined zamiast undefined:

λ> case Foo undefined of Foo _ -> True 
True 

Ten mecz uda, bo nie jest prawidłową wartość Foo że to jest dopasowana na. Zdarza się, że zawiera undefined, ale to nie ma znaczenia, ponieważ nie używamy go - patrzymy tylko na najwyższy konstruktor.


Oprócz wszystkich linków, które podałeś, możesz znaleźć odpowiednie this article.

Powiązane problemy