2017-01-31 27 views
9

wyjątki od Edwarda Kmetta biblioteka nie zapewnia instancji MonadMask dla ExceptT.Dlaczego nie ma instancji MonadMask dla narzędzia ExceptT?

Ben Gamari once asked about this a następnie doszedł do wniosku, że wyjaśniono to w dokumentacji. Jest to najbliżej istotne wyglądające przejście mogę znaleźć:

Zauważ, że ten pakiet ma zapewnić wystąpienie MonadMask dla CatchT. Ta instancja jest ważna tylko wtedy, gdy podstawowa monada nie zapewnia możliwości dostarczenia wielu wyjść. Na przykład: IO lub Either byłyby nieprawidłowymi monadami podstawowymi, ale akceptowalne byłyby Reader lub State.

Ale jego znaczenie nie jest dla mnie oczywiste. Co oznacza "wiele wyjść" i dlaczego blokuje instancję MonadMask?

Michael Snoyman also writes:

[...] 'MonadMask', co pozwala na zagwarantowanie prowadzony jest to, że niektóre działania, nawet w obecności wyjątkami (zarówno synchronicznych i asynchronicznych). Aby zapewnić tę gwarancję, monada stack musi być w stanie kontrolować swój przepływ wykonania. W szczególności wyklucza to wystąpienia dla [...] Monad z wieloma punktami wyjścia, takimi jak ErrorT przez IO.

Być może byłoby bardziej jasne zadać to alternatywny pytanie: Jeśli uchylenie transformatory i rozważyć nieco prostszy typ:

data IOEither a = IOEither { unIOEither :: IO (Either String a) } 
    deriving Functor 

To wydaje że możemy rzeczywiście napisać instancji MonadMask :

instance Applicative IOEither where 
    pure = IOEither . return . Right 
    IOEither fIO <*> IOEither xIO = IOEither $ 
     fIO >>= either (return . Left) (\f -> (fmap . fmap) f xIO) 

instance Monad IOEither where 
    IOEither xIO >>= f = IOEither $ 
     xIO >>= either (return . Left) (\x -> unIOEither (f x)) 

instance MonadThrow IOEither where 
    throwM e = IOEither (throwM @IO e) 

instance MonadCatch IOEither where 
    catch (IOEither aIO) f = IOEither $ catch @IO aIO (unIOEither . f) 

instance MonadMask IOEither where 
    mask f = IOEither $ mask @IO $ \restore -> 
     unIOEither $ f (IOEither . restore . unIOEither) 
    uninterruptibleMask f = IOEither $ uninterruptibleMask @IO $ \restore -> 
     unIOEither $ f (IOEither . restore . unIOEither) 

Czy ta instancja, którą napisałem, nie działa poprawnie?

+0

Czytelnicy zainteresowani w tej kwestii powinny również rozważyć Snoyman z [* Wyjątki Dobrych Praktyk w Haskell *] (https://www.fpcomplete.com/blog/2016/11/exceptions-best-practices-haskell) dla opinia, dlaczego 'IO (albo String a)' po prostu nie jest typem, którego powinno się używać. –

Odpowiedz

6

Poniżej znajduje się program, który demonstruje problem z twoimi wystąpieniami: Możesz wyjść wcześniej z Left, co spowoduje, że finalizator nigdy nie zostanie uruchomiony. Jest to sprzeczne z prawem określonym w dokumentach dotyczących MonadMask, które wymagają, aby dla f `finally` gg została wykonana niezależnie od tego, co stanie się w f. Powodem finalizator nigdy nie jest prowadzony jest dość prosta: Jeśli nie jest wyjątek finally (lub bracket który jest jak finally jest realizowany) używa tylko >>= uruchomić finalizatora potem jednak >>= nie wykonuje odpowiedniego argumentu, jeśli lewy zwraca Left.

data IOEither a = IOEither { unIOEither :: IO (Either String a) } 
    deriving Functor 

instance Applicative IOEither where 
    pure = IOEither . return . Right 
    IOEither fIO <*> IOEither xIO = IOEither $ 
     fIO >>= either (return . Left) (\f -> (fmap . fmap) f xIO) 

instance Monad IOEither where 
    IOEither xIO >>= f = IOEither $ 
     xIO >>= either (return . Left) (\x -> unIOEither (f x)) 

instance MonadThrow IOEither where 
    throwM e = IOEither (throwM @IO e) 

instance MonadCatch IOEither where 
    catch (IOEither aIO) f = IOEither $ catch @IO aIO (unIOEither . f) 

instance MonadMask IOEither where 
    mask f = IOEither $ mask @IO $ \restore -> 
     unIOEither $ f (IOEither . restore . unIOEither) 
    uninterruptibleMask f = IOEither $ uninterruptibleMask @IO $ \restore -> 
     unIOEither $ f (IOEither . restore . unIOEither) 

instance MonadIO IOEither where 
    liftIO x = IOEither (Right <$> x) 

main :: IO() 
main = void $ unIOEither $ finally (IOEither (return (Left "exit"))) 
            (liftIO (putStrLn "finalizer")) 
+0

Dokładnie w prawo. Właśnie dlatego pominęliśmy instancję. Zmęczyliśmy nasze mózgi sposobami naprawienia tego i byłoby miło mieć to, ale nie przeszło to oczywistego prawa finalizatora. Ostatecznie zdecydowaliśmy, że nie będziemy mieć nielegalnej (jeśli "oczywistej") instancji lub szukamy frazowania, które pozwoliłoby nam osłabić prawa w sposób podatny na błędy użytkownika. –

+0

@Edward KMETT Wydaje się, że możemy napisać specjalistyczną operację podobną do nawiasu, np. 'BracketE :: IO a -> (a -> IO b) -> (a -> WYCIĄG IO c) -> WYCIĄG IO c'' , nie moglibyśmy? Jeden, który nie był zależny od funkcji 'MonadMask' i używał' 'nawiasu' '' bezpośrednio. – danidiaz

+1

@danidiaz: Zobacz gałąź HEAD. https://github.com/ekmett/exceptions/commit/e8cd863ebaf1357091dc7df331bf5c9e73ac21a7 od krótkiego czasu dodał coś niewyraźnie w tym duchu. Obecnie trzęsimy nim, aby sprawdzić, czy nam się to podoba. –

1

Klasa dla monad, które przewidują możliwość uwzględnienia wszystkich możliwych punktów wyjścia z obliczeń i zamaskowania asynchronicznych wyjątki. Oparte na kontynuacji monady i stosy, takie jak ErrorT e IO, które zapewniają wiele trybów niepowodzenia, są nieprawidłowymi instancjami tej klasy.

Podczas korzystania ErrorT/ExceptT z IO posiadające „wiele punktów wyjścia” odnosi się do faktu, że można mieć albo wyjątek czasu wykonywania lub wyjątek rzucony w monady. Każda z nich zakończy obliczenia.

runExceptT $ do 
    error "This is an exit point." 
    throwError "This is another exit point." 
    return 23 

To byłoby możliwe, aby napisać MonadMask dla ExceptT który byłby ważny dla wszystkich ExceptT e m a z warunku, że podstawowe monada m jest nie IO. Stąd ogromne ostrzeżenie o używaniu CatchT z IO (spowoduje to unieważnienie instancji MonadMask).

+0

Czy mój 'IOE'' nie jest izomorficzny dla' ExceptT String IO' - co oznacza "podstawową monadę" * to * 'IO'? Możesz zakończyć obliczenia w Monadzie 'IOEither' za pomocą polecenia' throwIO' lub 'return. Left'. –

Powiązane problemy