Obsługa wyjątków w Haskell

79

Potrzebuję pomocy, aby zrozumieć użycie trzech funkcji Haskella

  • try ( Control.Exception.try :: Exception e => IO a -> IO (Either e a))
  • złapać ( Control.Exception.catch :: Exception e => IO a -> (e -> IO a) -> IO a)
  • uchwyt ( Control.Exception.handle :: Exception e => (e -> IO a) -> IO a -> IO a)

Muszę wiedzieć kilka rzeczy:

  1. Kiedy używać której funkcji?
  2. Jak używać tej funkcji na prostym przykładzie?
  3. Jaka jest różnica między zaczepem a uchwytem? Mają prawie ten sam podpis, tylko w innej kolejności.

Spróbuję spisać moje próby i mam nadzieję, że możesz mi pomóc:

próbować

Mam taki przykład:

x = 5 `div` 0
test = try (print x) :: IO (Either SomeException ())

Mam dwa pytania:

  1. Jak mogę ustawić niestandardowe wyjście błędu?

  2. Co mogę zrobić, aby ustawić wszystkie błędy na SomeException, więc nie muszę pisać :: IO (Either SomeException())

złap / spróbuj

Czy możesz mi pokazać krótki przykład z niestandardowym wyjściem błędu?

develhevel
źródło

Odpowiedzi:

132

Kiedy używać której funkcji?

Oto zalecenie z dokumentacji Control.Exception:

  • Jeśli chcesz zrobić porządki w przypadku, gdy jest wyjątek, stosowanie finally, bracketlubonException .
  • Aby odzyskać po wyjątku i zrobić coś innego, najlepszym wyborem jest użycie jednego z plików try rodziny.
  • ... chyba że odzyskujesz z asynchronicznego wyjątku, w takim przypadku użyj catchlub catchJust.

try :: Exception e => IO a -> IO (albo ea)

trypodejmuje IOakcję do uruchomienia i zwraca plik Either. Jeśli obliczenia się powiodły, wynik jest zawijany w Rightkonstruktorze. (Myśl dobrze, a nie źle). Jeśli akcja rzuciła wyjątek określonego typu , jest on zwracany w Leftkonstruktorze. Jeśli wyjątek nie był odpowiedniego typu, propaguje dalej w górę stosu. Określenie SomeExceptionjako typu spowoduje przechwycenie wszystkich wyjątków, co może być dobrym pomysłem lub nie.

Zauważ, że jeśli chcesz złapać wyjątek z czystego obliczenia, będziesz musiał użyć evaluatedo wymuszenia oceny w try.

main = do
    result <- try (evaluate (5 `div` 0)) :: IO (Either SomeException Int)
    case result of
        Left ex  -> putStrLn $ "Caught exception: " ++ show ex
        Right val -> putStrLn $ "The answer was: " ++ show val

catch :: Exception e => IO a -> (e -> IO a) -> IO a

catchjest podobny do try. Najpierw próbuje uruchomić określoną IOakcję, ale jeśli zostanie zgłoszony wyjątek, program obsługi otrzymuje wyjątek, aby uzyskać alternatywną odpowiedź.

main = catch (print $ 5 `div` 0) handler
  where
    handler :: SomeException -> IO ()
    handler ex = putStrLn $ "Caught exception: " ++ show ex

Jest jednak jedna ważna różnica. Podczas korzystania catchz programu obsługi nie można go przerwać przez wyjątek asynchroniczny (tj. Wyrzucony z innego wątku przez throwTo). Próby wywołania asynchronicznego wyjątku będą blokowane do momentu zakończenia działania programu obsługi.

Zwróć uwagę, że catchw Preludium jest coś innego , więc możesz chcieć to zrobić import Prelude hiding (catch).

handle :: Exception e => (e -> IO a) -> IO a -> IO a

handle jest po prostu catch z argumentami w odwrotnej kolejności. To, którego użyć, zależy od tego, co sprawia, że ​​twój kod jest bardziej czytelny, lub który pasuje lepiej, jeśli chcesz używać częściowej aplikacji. Poza tym są identyczne.

tryJust, catchJust and handleJust

Należy zauważyć, że try, catchi handlezłapie wszystkie wyjątki podanego / wywnioskować typu. tryJusti przyjaciele pozwalają ci określić funkcję selektora, która filtruje wyjątki, które chcesz obsłużyć. Na przykład wszystkie błędy arytmetyczne są typu ArithException. Jeśli chcesz tylko złapać DivideByZero, możesz:

main = do
    result <- tryJust selectDivByZero (evaluate $ 5 `div` 0)
    case result of
        Left what -> putStrLn $ "Division by " ++ what
        Right val -> putStrLn $ "The answer was: " ++ show val
  where
    selectDivByZero :: ArithException -> Maybe String
    selectDivByZero DivideByZero = Just "zero"
    selectDivByZero _ = Nothing

Uwaga o czystości

Zauważ, że ten typ obsługi wyjątków może mieć miejsce tylko w przypadku nieczystego kodu (np. IOMonady). Jeśli potrzebujesz obsługiwać błędy w czystym kodzie, powinieneś spojrzeć na zwracanie wartości za pomocą Maybelub Eitherzamiast tego (lub innego algebraicznego typu danych). Jest to często preferowane, ponieważ jest bardziej wyraźne, więc zawsze wiesz, co może się zdarzyć i gdzie. Control.Monad.ErrorTakie monady ułatwiają obsługę tego typu błędów.


Zobacz też:

hammar
źródło
8
dość pouczające, ale jestem zaskoczony, że pominąłeś ogólną zasadę w dokumentacji Control.Exception. Tj. „Użyj try, chyba że odzyskujesz po asynchronicznym wyjątku, w takim przypadku użyj catch
John L
2

Widzę, że jedną rzeczą, która również cię denerwuje (twoje drugie pytanie) jest pisanie :: IO (Either SomeException ())i irytowało mnie to.

Zmieniłem teraz kod z tego:

let x = 5 `div` 0
result <- try (print x) :: IO (Either SomeException ())
case result of
    Left _ -> putStrLn "Error"
    Right () -> putStrLn "OK"

Do tego:

let x = 5 `div` 0
result <- try (print x)
case result of
    Left (_ :: SomeException) -> putStrLn "Error"
    Right () -> putStrLn "OK"

Aby to zrobić, musisz użyć ScopedTypeVariablesrozszerzenia GHC, ale myślę, że estetycznie warto.

Emmanuel Touzery
źródło
1

Re: pytanie 3: zaczep i uchwyt są takie same (znalezione przez hoogle ). Wybór, którego użyć, będzie zwykle zależał od długości każdego argumentu. Jeśli akcja jest krótsza, użyj haczyka i odwrotnie. Prosty przykład klamki z dokumentacji:

do handle (\NonTermination -> exitWith (ExitFailure 1)) $ ...

Można również przypuszczalnie użyć funkcji handle, aby utworzyć niestandardową procedurę obsługi, którą można następnie przekazać, np. (na podstawie dokumentacji):

let handler = handle (\NonTermination -> exitWith (ExitFailure 1))

Niestandardowe komunikaty o błędach:

do       
    let result = 5 `div` 0
    let handler = (\_ -> print "Error") :: IOException -> IO ()
    catch (print result) handler
Boris
źródło