2012-11-06 17 views
6

Ponieważ wcześniej uprościłem w moim other question, chciałbym podać tutaj wyraźniejszy przykład.Elegancka obudowa/obsługa błędów w sekwencyjnych monadach

W jaki sposób mogę obsłużyć sytuacje, w których muszę sprawdzić warunki certyfikując w sposób sekwencyjny bez zagnieżdżania wielu przypadków? Przez "sekwencyjny sposób" rozumiem uzyskanie wartości (np. Ze standardowego wejścia), sprawdzenie tej wartości dla określonego warunku i w zależności od wyniku uzyskania innej wartości i tak dalej.

Przykład:

sequen :: IO String 
sequen = do 
    a <- getLine 
    case a of 
    "hi" -> do 
     putStrLn "hello!" 
     b <- getLine 
     case b of 
     "how are you?" -> do 
      putStrLn "fine, thanks" 
      return "nice conversation" 
     _ -> return "error 2" 
    _ -> return "error 1" 

wiem, że istnieją lepsze sposoby, aby napisać taki czat bota, należy po prostu wykazać sekwencyjną naturę problemu. Jak widać, w przypadku każdego zagnieżdżonego przypadku kod również jest wcięty głębiej.

Czy istnieje sposób na lepszą strukturę takiego kodu? Zastanawiam się nad obsługą "błędów" w jednym miejscu i opisywaniem "ścieżki sukcesu" bez obsługi błędów rozproszonych po niej.

Odpowiedz

20

Oczywiście. Właśnie do tego stworzono EitherT. Możesz go pobrać z Control.Monad.Trans.Either w pakiecie eitherT.

import Control.Monad.Trans.Class 
import Control.Monad.Trans.Either 

main = do 
    e <- runEitherT $ do 
     a <- lift getLine 
     case a of 
      "hi" -> lift $ putStrLn "hello!" 
      _ -> left 1 
     b <- lift getLine 
     case b of 
      "how are you?" -> lift $ putStrLn "fine, thanks!" 
      _    -> left 2 
     return "nice conversation" 
    case e of 
     Left n -> putStrLn $ "Error - Code: " ++ show n 
     Right str -> putStrLn $ "Success - String: " ++ str 

EitherT przerywa bieżący blok kodu, gdy napotka left oświadczenie, a ludzie zwykle używana do wskazania warunków błędów.

Typ bloku wewnętrznego to EitherT Int IO String. Gdy otrzymasz runEitherT, otrzymasz IO (Either Int String). Typ Left odpowiada przypadkowi, w którym mu się nie udało z wartością left, a wartość Right oznacza, że ​​pomyślnie osiągnęła koniec bloku.

+0

Awesome. Dokładnie to, co chciałem wiedzieć. Dziękuję Ci! –

+2

Na notatce EitherT napisałem post na blogu, który został całkiem dobrze przyjęty: http://ocharles.org.uk/blog/posts/2012-07-24-in-praise-of-EitherT.html – ocharles

+0

@ocharles Czytałem to jakiś czas temu i pomyślałem, że powinno to być rozwiązanie mojego problemu, ale nie mogłem go zastosować do tej pory. Możesz umieścić link do tego pytania w swoim poście :) –

0

Ostrzeżenie: odpowiedź od początkującego Haskella.

Możesz uniknąć tego rodzaju schodów dzięki Monadzie Być może. Dobry przykład na początku this chapter

Jednak chcesz coś podobnego z monadyczną (prawdopodobnie jest taka), ponieważ zwracasz kody błędów.

Podstawową ideą jest to, że gdy pojawi się błąd "Lewa 1", spowodujesz zwarcie wszelkich przyszłych kroków (z powodu leniwej oceny).

+0

Ah, widzę, że byłem na dobrej drodze, zgodnie z @Gabriel poniżej. –

+0

Z powodu leniwej oceny? Lub z powodu definicji monady dla 'Albo? –

1

Ponieważ koniecznie jesteś w monadzie IO, lepiej jest użyć możliwości obsługi błędów monady IO, zamiast układać monadę błędu na szczycie IO. Unika wszystkich ciężkich lift ING:

import Control.Monad (unless) 
import Control.Exception (catch) 
import Prelude hiding (catch) 
import System.IO.Error (ioeGetErrorString) 

main' = do 
    a <- getLine 
    unless (a == "hi") $ fail "error 1" 
    putStrLn "hello!" 
    b <- getLine 
    unless (b == "how are you?") $ fail "error 2" 
    putStrLn "fine, thanks" 
    return "nice conversation" 

main = catch main' $ return . ioeGetErrorString 

W tym przypadku, twoje błędy są po prostu String s, które są rzucane przez IO „s fail jako userError. Jeśli chcesz rzucić jakiś inny typ, musisz użyć throwIO zamiast fail.

5

Napisałem serię postów na jakiś czas, przechodząc przez moje własne nauki typów Either & EitherT.Można go przeczytać tutaj: http://watchchrislearn.com/blog/2013/12/01/working-entirely-in-eithert/

używam pakiet errors dostać kilka miłych pomocników wokół używając EitherT (left i right funkcji na przykład do powrotu zniesione wersje Left i Right).

Wyodrębniając swoje potencjalne warunki niepowodzenia do swoich własnych pomocników, możesz sprawić, że linia główna twojego kodu zostanie odczytana całkowicie sekwencyjnie, bez wyników sprawdzania spraw.

Z tego wpisu widać, jak sekcja runEitherT jest sekwencyjnym fragmentem pracy, po prostu ma mechanikę awarii EitherT. Oczywiście ten kod jest dość wymyślny, aby pokazać, jak również MaybeT gra wewnątrz EitherT. W prawdziwym kodzie byłaby to opowieść, którą chciałbyś opowiedzieć, z pojedynczym końcem na końcu: Left/Right.

import Control.Error 
import Control.Monad.Trans 

-- A type for my example functions to pass or fail on. 
data Flag = Pass | Error 

main :: IO() 
main = do 
    putStrLn "Starting to do work:" 

    result <- runEitherT $ do 
     lift $ putStrLn "Give me the first input please:" 
     initialText <- lift getLine 
     x <- eitherFailure Error initialText 

     lift $ putStrLn "Give me the second input please:" 
     secondText <- lift getLine 
     y <- eitherFailure Pass (secondText ++ x) 

     noteT ("Failed the Maybe: " ++ y) $ maybeFailure Pass y 

    case result of 
    Left val -> putStrLn $ "Work Result: Failed\n " ++ val 
    Right val -> putStrLn $ "Work Result: Passed\n " ++ val 

    putStrLn "Ok, finished. Have a nice day" 

eitherFailure :: Monad m => Flag -> String -> EitherT String m String 
eitherFailure Pass val = right $ "-> Passed " ++ val 
eitherFailure Error val = left $ "-> Failed " ++ val 

maybeFailure :: Monad m => Flag -> String -> MaybeT m String 
maybeFailure Pass val = just $ "-> Passed maybe " ++ val 
maybeFailure Error _ = nothing 
+0

Dobry przykład! Dzięki za wskazanie, że możesz umieścić sprawy w oddzielnych funkcjach. Sprawia, że ​​główna funkcja jest znacznie przyjemniejsza do odczytania. –

Powiązane problemy