Throw error haskell

These are the four types of error handling that are standard and widely used in the Haskell world, as of 2014.

These are the four types of error handling that are standard and
widely used in the Haskell world, as of 2014.

There exist some other libraries like attempt (like Either, but where you don’t care or know the type of the exception) and control-monad-exception which implements a checked exception monad, etc. but the following ones are the standard ones seen in the wild.

Contents

  • 1 Exception
  • 2 Error (pure code)
  • 3 Error using the Either type
  • 4 No value using the Maybe type

Exception

An unexpected code path, one that rarely but can happen and can be handled if needs be. Typically caused by IO going wrong in some way, like the machine running out of swap and your program terminating, a file not existing, etc. The most basic functions are:

  • throw :: Exception e => e -> a
  • try :: Exception e => IO a -> IO (Either e a)

from Control.Exception.

Say you were writing a library to do things on reddit, you would define an exception type in your API:

data RedditException
  = Couldn'tUpvote
  | CommentFailed
  | LoginFailed !Text
  | ConnectFailure !HttpError
  deriving (Show,Typeable)

instance Exception RedditException

login :: Details -> IO ()
login details = do
  code <- tryLogin details
  case code of
    (200,val) -> setLoginContext val
    (_,err)   -> throw (LoginFailed err)

Then later you might write try (login ) or catch (login ) ((e :: RedditException) -> ) to handle the exception, if needed. Another exception might be a connection failure.

See Control.Exception for more detail and related functions.

Error (pure code)

Some pure functions contain calls to error, causing the evaluation to stop and crash:

head :: [a] -> a
head (x:_) = x
head []    = error "empty list"

Now suppose someone writes head ages and unexpectedly, ages is an empty list. If you are trying to take the head of an empty list your program logic is simply broken.

A solution here is to avoid the head function and use listToMaybe from Data.Maybe.

case listToMaybe ages of
  Nothing -> defaultAge
  Just first -> first

Alternatively, these errors can be caught from IO monad by using evaluate and try from Control.Exception. Ideally you should avoid partial functions like head, but sometimes this is not an option (e.g. when using an external library)

Error using the Either type

An expected return value: Either SomeError a The type indicates that an error is common, but doesn’t mean your program is broke. Rather that some input value wasn’t right. Typically used by parsers, consumers, that are pure and often error out.

data ParseError = ParseError !Pos !Text

So this type describes exactly what is going on:

runParser :: Parser a -> Text -> Either ParseError a

Take a parser of a, some text to parse, and return either a parser error or a parsed a. Typical usage would be:

main = do
  line <- getLine
  case runParser emailParser line of
    Right (user,domain) -> print ("The email is OK.",user,domain)
    Left (pos,err) -> putStrLn ("Parse error on " <> pos <> ": " <> err)

Or depending on the code one might opt instead to use a deconstructing function:

main = do
  line <- getLine
  either (putStrLn . ("Parse error: " <>) . show)
         (print . ("The email is OK.",))
         (runParser emailParser line)

No value using the Maybe type

There is simply no value there. This isn’t a problem in the system. It means you don’t care why there isn’t a value, or you already know. Maybe a

Typical example:

lookup :: Eq a => a -> [(a,b)] -> Maybe b

That is, take some key a that can be compared for equality, and a list of pairs where the first is the same type of the key a and maybe return the b of the pair, or nothing.

So one might pattern match on this:

case lookup name person of
  Nothing -> "no name specified"
  Just name -> "Name: " <> name

Or use a deconstructing function:

maybe "no name specified"
      ("Name: " <>)
      (lookup name person)

Again, depends on the code and the person writing it whether an explicit case is used. Often monads like Maybe are composed to make a chain of possibly-nothing values:

lookup "height" profile >>=
parseInt >>=
flip lookup recommendedSizes

So lookup a height from a person’s profile (might not exist), parse it as integer (might not parse), then use that as a key to lookup from a mapping list of age to clothes size: [(Int,Text)]

Example functions that performs data validation or user validation:

For function that performs data validation it is better to return the
result as an Maybe or Either type instead of raise an Exception. Maybe
is better for functions that only have one possible failure or is not
necessary to know the type of failure. The Either type is useful to
report error, failure information or invalid data.

import Text.Read (readMabye)

:{
validateAge :: String -> Int  
validateAge input =
  case readMaybe input of
    Nothing  -> error "Invalid age. Not a number"
    Just age -> case age of 
                _ | age < 0    -> error "Error: Invalid age. It must be greater than zero."
                _ | age <= 18  -> error "Error: Below legal age to sign the contract."
                _ | age > 200  -> error "Error: Invalid age. Impossible age."
                _              -> age   
:}

> validateAge "asdfa" 
*** Exception: Invalid age. Not a number
CallStack (from HasCallStack):
  error, called at <interactive>:1083:17 in interactive:Ghci54

> validateAge "-100" 
*** Exception: Error: Invalid age. It must be greater than zero.
CallStack (from HasCallStack):
  error, called at <interactive>:1085:35 in interactive:Ghci54

> validateAge "16" 
*** Exception: Error: Below legal age to sign the contract.
CallStack (from HasCallStack):
  error, called at <interactive>:1086:35 in interactive:Ghci54
>

> validateAge "300" 
*** Exception: Error: Invalid age. Impossible age.
CallStack (from HasCallStack):
  error, called at <interactive>:1087:35 in interactive:Ghci54
>

> validateAge "30" 
30
it :: Int

> validateAge "20" 
20
it :: Int

> validateAge "35"  
35
it :: Int
>

This function can be refactored to:

import Text.Read (readMabye)

:{
validateAge :: String -> Either String Int  
validateAge input =
  case readMaybe input of
    Nothing  -> Left "Invalid input. Not a number"
    Just age -> case age of 
                _ | age < 0    -> Left "Error: Invalid age. It must be greater than zero."
                _ | age <= 18  -> Left "Error: Below legal age to sign the contract."
                _ | age > 200  -> Left "Error: Invalid age. Impossible age."
                _              -> Right age   
:}

> validateAge "-100"
Left "Error: Invalid age. It must be greater than zero."
it :: Either String Int
> validateAge "16"
Left "Error: Below legal age to sign the contract."
it :: Either String Int
> validateAge "300"
Left "Error: Invalid age. Impossible age."
it :: Either String Int
> validateAge "400"
Left "Error: Invalid age. Impossible age."
it :: Either String Int
> validateAge "20"
Right 20
it :: Either String Int
> validateAge "19"
Right 19
it :: Either String Int
>

> mapM_ print $ map validateAge ["safsdf", "-100", "garbage", "400", "7", "15", "20", "25"]
Left "Invalid input. Not a number"
Left "Error: Invalid age. It must be greater than zero."
Left "Invalid input. Not a number"
Left "Error: Invalid age. Impossible age."
Left "Error: Below legal age to sign the contract."
Left "Error: Below legal age to sign the contract."
Right 20
Right 25
it :: ()
>

Algebraic data types are more friendly to pattern matching than
strings. So the code above could be refactored to:

import Text.Read (readMabye)

:{ 
data AgeError = AgeInvalidInput
              | AgeBelowLegalAge
              | AgeImpossible
              deriving (Eq, Show, Read)
:}


:{
validateAge :: String -> Either AgeError Int  
validateAge input =
  case readMaybe input of
    Nothing  -> Left AgeInvalidInput
    Just age -> case age of 
                _ | age < 0    -> Left AgeImpossible
                _ | age <= 18  -> Left AgeBelowLegalAge
                _ | age > 200  -> Left AgeImpossible
                _              -> Right age   
:}

> map (input -> (input, validateAge input)) ["safsdf", "-100", "garbage", "400", "7", "15", "20", "25"]
[("safsdf",Left AgeInvalidInput),("-100",Left AgeImpossible),("garbage",Left AgeInvalidInput),
 ("400",Left AgeImpossible),("7",Left AgeBelowLegalAge),
 ("15",Left AgeBelowLegalAge),("20",Right 20),
 ("25",Right 25)]
it :: [([Char], Either AgeError Int)]
> 
> 

> mapM_ print $ map validateAge ["safsdf", "-100", "garbage", "400", "7", "15", "20", "25"]
Left AgeInvalidInput
Left AgeImpossible
Left AgeInvalidInput
Left AgeImpossible
Left AgeBelowLegalAge
Left AgeBelowLegalAge
Right 20
Right 25
it :: ()
>

:{
showAgeError :: AgeError -> String
showAgeError age =
   case age of
     AgeBelowLegalAge ->  "Error: Below legal age to sign the contract."
     AgeImpossible     ->  "Error: Invalid age. Impossible age."
     AgeInvalidInput    ->  "Invalid input. Not a number"
:}

> showAgeError AgeImpossible
"Error: Invalid age. Impossible age."
it :: String
> showAgeError AgeInvalidInput
"Invalid input. Not a number"
it :: String
> showAgeError AgeImpossible
"Error: Invalid age. Impossible age."
it :: String
>


:{
mapLeft :: (e -> b) -> Either e a -> Either b a
mapLeft fn value =
  case value of
    Right a -> Right a
    Left  e -> Left (fn e)
:}

> mapLeft showAgeError $ validateAge "20000" 
Left "Error: Invalid age. Impossible age."
it :: Either String Int
> mapLeft showAgeError $ validateAge "-100" 
Left "Error: Invalid age. Impossible age."
it :: Either String Int
> mapLeft showAgeError $ validateAge "10" 
Left "Error: Below legal age to sign the contract."
it :: Either String Int
> mapLeft showAgeError $ validateAge "15" 
Left "Error: Below legal age to sign the contract."
it :: Either String Int
> mapLeft showAgeError $ validateAge "25" 
Right 25
it :: Either String Int
> mapLeft showAgeError $ validateAge "36" 
Right 36
it :: Either String Int
>

Refactoring the function to validate multiple data:

import Text.Read (readMabye)

:{ 
data AgeError = AgeInvalidInput
              | AgeBelowLegalAge
              | AgeImpossible
              deriving (Eq, Show, Read)
:}

type UserData = (String, Int)


:{
data UserDataError = UserAgeError AgeError
                   | UserNameError
                   deriving(Eq, Show, Read)
:}

:{
mapLeft :: (e -> b) -> Either e a -> Either b a
mapLeft fn value =
  case value of
    Right a -> Right a
    Left  e -> Left (fn e)
:}


:{
validateAge :: String -> Either UserDataError Int  
validateAge input = mapLeft UserAgeError $ validateAgeAux input 
  where
    validateAgeAux input = 
      case readMaybe input of
        Nothing  -> Left AgeInvalidInput
        Just age -> case age of 
                    _ | age < 0    -> Left AgeImpossible
                    _ | age <= 18  -> Left AgeBelowLegalAge
                    _ | age > 200  -> Left AgeImpossible
                    _              -> Right age   
:}


:{
validateName :: String -> Either UserDataError String   
validateName name =
  case name of
    "" -> Left $ UserNameError
    _  -> Right name
:}

:{
validateUser :: String -> String -> Either UserDataError UserData
validateUser name age = do
  userName <- validateName name
  userAge  <- validateAge age
  return (userName, userAge)
:}





> validateAge "-200" 
Left (UserAgeError AgeImpossible)
it :: Either UserDataError Int
> validateAge "1000" 
Left (UserAgeError AgeImpossible)
it :: Either UserDataError Int
> validateAge "30" 
Right 30
it :: Either UserDataError Int
> validateAge "25" 
Right 25
it :: Either UserDataError Int
> validateAge "16" 
Left (UserAgeError AgeBelowLegalAge)
it :: Either UserDataError Int

> validateAge "25" 
Right 25
it :: Either UserDataError Int
>

> validateName "" 
Left UserNameError
it :: Either UserDataError String
> 
> validateName "John" 
Right "John"
it :: Either UserDataError String
>

> validateUser "John" "300" 
Left (UserAgeError AgeImpossible)
it :: Either UserDataError UserData
> validateUser "John" "15" 
Left (UserAgeError AgeBelowLegalAge)
it :: Either UserDataError UserData
> validateUser "John" "-100" 
Left (UserAgeError AgeImpossible)
it :: Either UserDataError UserData
> validateUser "" "-100" 
Left UserNameError
it :: Either UserDataError UserData
>

> validateUser "John" "20" 
Right ("John",20)
it :: Either UserDataError UserData
>

Exceptions

Exceptions
Creative Commons image by gynti

Most languages make a distinction between values that represent failure
(errors) and the mechanism to abort computations and unwind the stack
(exceptions.) Haskell is unique in that the type system makes it safe
and easy to build failure into types instead of lumping everything into
something like NULL or -1.

It also stands out by supporting exceptions through a library of
functions and types instead of directly in the syntax of the language.
The fact that there’s no dedicated keywords for exceptions might seem
weird until you discover how flexible and expressive Haskell is.

This presentation aims to show how closely related errors and exceptions
are, and how to keep them separate.

Haskell Exceptions

  • No dedicated syntax

  • Very limited in Haskell 2010

  • Expanded by GHC

Type Inhabitants

In order to understand how exceptions work we first need to talk about
type inhabitants and bottom.

The Bool type is a very simple type that doesn’t use any type
variables and only has 2 data constructors. This means that there can
only be 2 unique values for this type. Or does it?

Bottom (⊥)

All types in Haskell support a value called bottom. This means that the
Bool type actually has 3 possible values. Exceptions and
non-termination are examples of bottom values.

The list below illustrates that bottom values aren’t a problem until
they’re evaluated.

bools :: [Bool]
bools = [False, True, undefined]

Creating ⊥

Haskell includes 2 functions for creating bottom values: undefined and
error. In GHC undefined is implemented using error and error
throws an exception.

You can create a bottom value directly by writing a non-terminating
function.

-- Raises exceptions in GHC:
undefined :: a
error :: String -> a

-- Non-termination:
badBoy :: a
badBoy = badBoy

Catching Exceptions (Inline)

Catching exceptions is straight forward as long as you remember that you
can only catch exceptions in the IO monad.

inline :: Int -> IO Int
inline x =
  catch (shortFuse x)
        ((_ex :: StupidException) -> return 0)

The second argument to catch is a function to handle a caught
exception. GHC uses the type of the function to determine if it can
handle the caught exception. If GHC can’t infer the type of the function
you’ll need to add a type annotation like in the example above. This
requires the ScopedTypeVariables extension.

If you want to handle more than one exception type you’ll need to use
something like the catches function. To catch all possible exceptions
you can catch the SomeException type since it’s at the top of the
exception type hierarchy. This isn’t generally wise and instead you
should use something like the bracket or finally functions.

One interesting thing to note is that GHC differs from Haskell 2010 with
regards to catch. Haskell 2010 states that catch should catch all
exceptions regardless of their type. Probably because those exceptions
would all be IOErrors.

Catching Exceptions (w/ a Helper)

Below is another example of catching exceptions. This time a helper
function with an explicit type signature is used to handle the
exception. This allows us to avoid inline type annotations and the
ScopedTypeVariables extension.

helper :: Int -> IO Int
helper x =
  catch (shortFuse x)
        handler
  where
    handler :: StupidException -> IO Int
    handler _ = return 0

Throwing Exceptions

Throwing exceptions is really easy, although you must be in the IO
monad to do so. Haskell 2010 provides a set of functions for creating
and raising exceptions.

Haskell 2010:

-- Create an exception.
userError :: String -> IOError

-- Raise an exception.
ioError :: IOError -> IO a

-- fail from the IO Monad is both.
fail = ioError . userError :: String -> IO a

Throwing Exceptions

GHC adds on to Haskell 2010 with functions like throwIO and throw.
The throw function allows you to raise an exception in pure code and
is considered to be a misfeature.

GHC:

shortFuse :: Int -> IO Int
shortFuse x =
  if x > 0
    then return (x - 1)
    else throwIO StupidException

Throwing from Pure Code

As mentioned above, GHC adds a throw function that allows you to raise
an exception from pure code. Unfortunately this makes it very difficult
to catch.

naughtyFunction :: Int -> Int
naughtyFunction x =
  if x > 0
    then x - 1
    else throw StupidException

Catching Exceptions From throw

You need to ensure that values are evaluated because they might contain
unevaluated exceptions.

In the example below you’ll notice the use of the «$!» operator. This
forces evaluation to WHNF so exceptions don’t sneak out of the catch
function as unevaluated thunks.

forced :: Int -> IO Int
forced x =
  catch (return $! naughtyFunction x)
        ((_ex :: StupidException) -> return 0)

Creating Custom Exceptions

Any type can be used as an exception as long as it’s an instance of the
Exception type class. Deriving from the Typeable class makes
creating the Exception instance trivial. However, using Typeable
means you need to enable the DeriveDataTypeable GHC extension.

You can also automatically derive the Show instance as with most other
types, but creating one manually allows you to write a more descriptive
message for the custom exception.

data StupidException = StupidException
  deriving (Typeable)

instance Show StupidException where
  show StupidException =
    "StupidException: you did something stupid"

instance Exception StupidException

Threads and Exceptions

Concurrency greatly complicates exception handling. The GHC runtime uses
exceptions to send various signals to threads. You also need to be very
careful with unevaluated thunks exiting from a thread when it
terminates.

Additional problems created by concurrency:

  • Exceptions are used to kill threads

  • Exceptions are asynchronous

  • Need to mask exceptions in critical code

  • Probably don’t want unevaluated exceptions leaking out

There’s a Package For That

Just use the async package.

Errors (Instead of Exceptions)

  • Explicit

  • Checked by the compiler

  • Way better than NULL or -1

Stupid

Haskell is great about forcing programmers to deal with problems at
compile time. That said, it’s still possible to write code which may not
work at runtime. Especially with partial functions.

The function below will throw an exception at runtime if it’s given an
empty list. This is because head is a partial function and only works
with non-empty lists.

stupid :: [Int] -> Int
stupid xs = head xs + 1

Better

Prefer errors to exceptions.

A better approach is to avoid the use of head and pattern match the
list directly. The function below is total since it can handle lists
of any length (including infinite lists).

Of course, if the list or its head is bottom (⊥) then this function will
throw an exception when the patterns are evaluated.

better :: [Int] -> Maybe Int
better []    = Nothing
better (x:_) = Just (x + 1)

Reusing Existing Functions

This is the version I like most because it reuses existing functions
that are well tested.

The listToMaybe function comes with the Haskell Platform. It takes a
list and returns its head in a Just. If the list is empty it returns
Nothing. Alternatively you can use the headMay function from the
Safe package.

reuse :: [Int] -> Maybe Int
reuse = fmap (+1) . listToMaybe

Providing Error Messages

Another popular type when dealing with failure is Either which allows
you to return a value with an error. It’s common to include an error
message using the Left constructor.

Beyond Maybe and Either it’s also common to define your own type
that indicates success or failure. We won’t discuss this further.

withError :: [Int] -> Either String Int
withError []    = Left "this is awkward"
withError (x:_) = Right (x + 1)

Maybe and Either

Maybe and Either are also monads!

If you have several functions that return one of these types you can use
do notation to sequence them and abort the entire block on the first
failure. This allows you to write short code that implicitly checks the
return value of every function.

Things tend to get a bit messy when you mix monads though…

Maybe and IO

The code below demonstrates mixing two monads, IO and Maybe. Clearly
we want to be able to perform I/O but we also want to use the Maybe
type to signal when a file doesn’t exist. This isn’t too complicated,
but what happens when we want to use the power of the Maybe monad to
short circuit a computation when we encounter a Nothing?

size :: FilePath -> IO (Maybe Integer)
size f = do
  exist <- fileExist f
  
  if exist
    then Just <$> fileSize f
    else return Nothing

Maybe and IO

Because IO is the outer monad and we can’t do without it, we sort of
lose the superpowers of the Maybe monad.

add :: FilePath -> FilePath -> IO (Maybe Integer)
add f1 f2 = do
  s1 <- size f1
  case s1 of
    Nothing -> return Nothing
    Just x  -> size f2 >>= s2 ->
      case s2 of
        Nothing -> return Nothing
        Just y  -> return . Just $ x + y

MaybeT

Using the MaybeT monad transformer we can make IO the inner monad
and restore the Maybe goodness. We don’t really see the benefit in the
sizeT function but note that its complexity remains about the same.

sizeT :: FilePath -> MaybeT IO Integer
sizeT f = do
  exist <- lift (fileExist f)
           
  if exist
    then lift (fileSize f)
    else mzero

MaybeT

The real payoff comes in the addT function. Compare with the add
function above.

addT :: FilePath -> FilePath -> IO (Maybe Integer)
addT f1 f2 = runMaybeT $ do
  s1 <- sizeT f1
  s2 <- sizeT f2
  return (s1 + s2)

Either and IO

This version using Either is nearly identical to the Maybe version
above. The only difference is that we can now report the name of the
file which doesn’t exist.

size :: FilePath -> IO (Either String Integer)
size f = do
  exist <- fileExist f

  if exist
    then Right <$> fileSize f
    else return . Left $ "no such file: " ++ f

Either and IO

To truly abort the add function when one of the files doesn’t exist
we’d need to replicate the nested case code from the Maybe example.
Here I’m cheating and using Either‘s applicative instance. However,
this doesn’t short circuit the second file test if the first fails.

add :: FilePath -> FilePath -> IO (Either String Integer)
add f1 f2 = do
  s1 <- size f1
  s2 <- size f2
  return ((+) <$> s1 <*> s2)

ErrorT

The ErrorT monad transformer is to Either what MaybeT is to
Maybe. Again, changing size to work with a transformer isn’t that
big of a deal.

sizeT :: FilePath -> ErrorT String IO Integer
sizeT f = do
  exist <- lift $ fileExist f

  if exist
    then lift $ fileSize f
    else fail $ "no such file: " ++ f

ErrorT

But it makes a big difference in the addT function.

addT :: FilePath -> FilePath -> IO (Either String Integer)
addT f1 f2 = runErrorT $ do
  s1 <- sizeT f1
  s2 <- sizeT f2
  return (s1 + s2)

Hidden/Internal ErrorT

The really interesting thing is that we didn’t actually have to change
size at all. We could have retained the non-transformer version and
used the ErrorT constructor to lift the size function into the
transformer. The MaybeT constructor can be used in a similar way.

addT' :: FilePath -> FilePath -> IO (Either String Integer)
addT' f1 f2 = runErrorT $ do
  s1 <- ErrorT $ size f1
  s2 <- ErrorT $ size f2
  return (s1 + s2)

Turning Exceptions into Errors

The try function allows us to turn exceptions into errors in the form
of IO and Either, or as you now know, ErrorT.

It’s not hard to see how flexible exception handling in Haskell is, in
no small part due to it not being part of the syntax. Non-strict
evaluation is the other major ingredient.

try :: Exception e => IO a -> IO (Either e a)

-- Which is equivalent to:
try :: Exception e => IO a -> ErrorT e IO a

Final Thought

  • Prefer Errors to Exceptions!

  • Don’t Write/Use Partial Functions!

Один из наиболее запутанных вопросов при изучении Haskell — это обработка исключений. Многие учебники, в том числе LYH, повествуют об исключениях, описанных в стандарте Haskell 98, создавая тем самым ошибочное впечатление, что в Haskell нельзя объявлять собственные исключения. А в RWH, например, в качестве «современных» функций для работы с исключениями называются throwDyn, catchDyn и прочие. В результате многие хаскелисты не понимают и боятся исключений, а асинхронные исключения так и вовсе считают какой-то черной магией. Благодаря этой небольшой заметке вы узнаете, как же на самом деле в Haskell обрабатываются исключения.

Основы

Проще всего дела обстоят с чистыми функциями. В 99 процентах случаев роль «исключений» в них играют монады Maybe и Either. Также специально для знающих толк месье предусмотрены трансформаторы монад MaybeT, EitherT и ErrorT. В общем, если вы пишите чистую функцию, и хотите как-то сообщить об ошибке, то вам почти наверняка нужен либо Maybe, либо Either. Правда, иногда это неудобно, и приходится все-таки пользоваться исключениями. Представьте на секунду мир, в котором функции head и div возвращают Maybe.

Для грязных функций, а также случаев, когда в чистой функции случилось нечто невозможное, есть исключения. Исключения в Haskell могут бросать как чистые, так и грязные функции, но ловить исключения при этом можно только в грязных функциях. Простейший способ бросить исключение состоит в том, чтобы вызвать всем знакомую функцию error:

ghci> :t error
error :: [Char] -> a
ghci> error «something impossible just happened»
*** Exception: something impossible just happened

Ловятся исключения при помощи функции catch:

ghci> :m + Control.Exception
ghci> :t catch
catch :: Exception e => IO a -> (e -> IO a) -> IO a
ghci> catch (error «fail!») $ e -> print (e :: ErrorCall)
fail!

… или функции handle, отличающейся от catch только порядком аргументов:

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

Как видите, здесь мы используем модуль Control.Exception. В Haskell нет какого-то специального синтаксиса для обработки исключений, все делается только при помощи функций и типов, сосредоточенных в этом модуле. Это, кстати, является прекрасным примером создания предметно-ориентированного языка при помощи комбинаторов. Если вам попадется туториал, рекомендующий использовать для обработки исключений функции из модулей Prelude и System.IO.Error, знайте, перед вами устаревшая информация. На самом деле, модуль System.IO.Error все еще используется, но только потому что в нем есть функции для работы с исключениями типа IOError (который представляет собой всего лишь синоним типа IOException), например, isDoesNotExistError, isPermissionError и другие. Еще один тонкий момент состоит в том, что в GHC до версии 7.6 модуль Prelude также экспортировал функцию catch, поэтому в коде приходилось писать:

import Prelude hiding (catch)

Если вы используете GHC 7.6 или старше, этого больше делать не нужно.

В чистых функциях исключения бросаются с помощью функции throw, а внутри монады IO — с помощью throwIO:

throw :: Exception e => e -> a
throwIO :: Exception e => e -> IO a

Функция error представляет собой всего лишь обертку над throw:

error :: [Char] -> a
error s = throw (ErrorCall s)

В отличие от throw функция throwIO гарантирует правильный порядок выполнения операций внутри монады IO. Аналогично при работе с транзакционной памятью вместо throw следует использовать функцию throwSTM.

Вы могли обратить внимание, что при использовании catch в обработчике исключения был указан точный тип исключения, ErrorCall. Если указать другой тип, исключение не будет поймано:

ghci> catch (error «fail!») $ e -> print (e :: IOException)
*** Exception: fail!

Возникает закономерный вопрос — а есть ли способ поймать исключение любого типа? Оказывается, что есть. Дело в том, что исключения в Haskell образуют иерархию, корнем которой является тип SomeException. Если указать этот тип, будет поймано любое исключение:

ghci> catch (error «fail!») $ e -> print (e :: SomeException)
fail!

Заметьте, что этот прием следует использовать только для отладки и освобождения ресурсов перед повторным бросанием исключения. Ловить SomeException в остальных случаях считается code smell.

Создание новых типов исключений

В модуле Control.Exception вы найдете немало различных типов исключений — ArithException, ErrorCall, IOException и другие. Также ничто не мешает объявлять собственные исключения. Но сначала немного теории.

Класс типов Exception представляет собой довольно простую фигню:

class (Typeable e, Show e) => Exception e where
  toException :: e -> SomeException
  fromException :: SomeException -> Maybe e

… а SomeException при этом определяется следующим образом:

{-# LANGUAGE ExistentialQuantification, DeriveDataTypeable #-}

data SomeException = forall e . Exception e => SomeException e
  deriving Typeable

Если вы раньше не сталкивались с экзистенциальными типами, загляните на Haskell Wiki. Это очень простое расширение GHC, разобраться в котором займет у вас максимум 5 минут. Несмотря на свою простоту, экзистенциальные типы позволяют использовать в Haskell элементы динамический типизации, а также строить иерархии типов (ООП в функциональном языке!), что, собственно, и делается в случае с SomeException.

Typeable также представляет собой совершенно обычный класс типов:

data TypeRep =
  — какое-то определение
  deriving (Eq, Ord, Show)

class Typeable a where
  typeOf :: a -> TypeRep

Вот как это примерно работает:

ghci> :m + Data.Typeable
ghci> typeOf True
Bool
ghci> :set -XDeriveDataTypeable
ghci> data X = X deriving Typeable
ghci> typeOf X
X

Пожалуйста — еще один пример рефлексии в Haskell.

Итак, чтобы объявить собственный тип исключения, нам всего лишь нужно ввести новый тип, являющийся экземпляром классов Show, Typeable и Exception. Давайте попробуем:

ghci> data MyError = MyError String deriving (Show, Typeable)
ghci> instance Exception MyError
ghci> (throw $ MyError «fail!») `catch` e -> print (e :: MyError)
MyError «fail!»

Надо же, работает!

Функция bracket и компания

Еще три функции, которые следует иметь в виду при работе с исключениями — это bracket, finally и onException:

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
finally :: IO a -> IO b -> IO a
onException :: IO a -> IO b -> IO a

Допустим, мы работаем с некоторым недешевым ресурсом, например, файловым дескриптором. Что произойдет, если во время работы с ним будет брошено исключение? Будет ли файл закрыт? На самом деле, будет, по крайней мере, при использовании GHC, но только когда до файлового дескриптора доберется сборщик мусора. А произойти это может спустя неопределенное время, за которое мы успеем наоткрывать еще кучу файлов. Налицо утечка ресурсов. Решение проблемы заключается в том, чтобы освободить ресурс сразу после окончания работы с ним, независимо от того, было брошено исключение, или нет.

Для этого и нужна функция bracket. Первым аргументом она принимает функцию, выделяющую ресурс, вторым — функцию, освобождающую ресурс, а третьим — функцию, непосредственно использующую этот ресурс. Ресурс освобождается независимо от того, было брошено при работе с ним исключение, или нет. Если было брошено исключение, bracket бросает его повторно после освобождения ресурса. Вот, например, как выглядит функция withFile из модуля System.IO:

withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
withFile name mode = bracket (openFile name mode) hClose

Функция finally похожа на bracket. Сначала вызывается функция, переданная первым аргументом, затем — вторым. Причем вторая функция будет вызвана всегда, независимо от того, бросит первая исключение, или нет. В отличие от finally функция onException вызывает функцию, переданную вторым аргументом, только в случае, если первая функция бросит исключение. Как onException, так и finally бросают пойманные исключения повторно.

Асинхронные исключения

До сих пор все было не слишком сложно, правда? Сейчас начнется самое интересное. Дело в том, что помимо обычных, синхронных исключений, в Haskell бывают еще и асинхронные исключения. Асинхронные исключения приходят выполняющимся потокам извне и могут выстреливать в любое время. Как было отмечено в заметке Работа с нитями/потоками в Haskell, в некоторых случаях система времени выполнения умеет обнаруживать возникновение дэдлоков и посылать заблокированным нитям исключение BlockedIndefinitelyOnMVar. Аналогично при работе с транзакционной памятью в этом случае посылается исключение BlockedIndefinitelyOnSTM. Еще асинхронные исключения приходят при переполнении стека или кучи, а также при нажатии пользователем Ctr+C:

ghci> threadDelay 5000000 `catch` e -> print (e :: AsyncException)
^Cuser interrupt

На самом деле, любая нить может послать любой другой нити абсолютно любое асинхронное исключение при помощи функции throwTo:

throwTo :: Exception e => ThreadId -> e -> IO ()

Итак, оказывается, в любое время может быть брошено любое исключение. Понятно, что в некоторых случаях это нежелательно. Например, рассмотрим такую реализацию функции bracket:

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket before after thing = do
  — 1. выделяем ресурс
  a <- before

  — 2. работаем с ресурсом, в случае исключения освобождаем его
  —    и повторно бросаем исключение
  r <- thing a `onException` after a

  — 3. если все ОК, освобождаем ресурс
  _ <- after a
  return r

Если асинхронное исключение придет где-то во время перехода от действия 1 к действию 2 или от действия 2 к действию 3, оно не будет перехвачено и ресурс благополучно утечет. Для решения этой проблемы предназначена функция mask:

mask :: ((IO a -> IO a) -> IO b) -> IO b

Эта функция имеет довольно сложный (и, на самом деле, несколько иной) тип, поэтому давайте лучше посмотрим, как она используется. Настоящая функция bracket, экспортируемой модулем Control.Exception, выглядит так:

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket before after thing =
  mask $ restore -> do
    a <- before
    r <- restore (thing a) `onException` after a
    _ <- after a
    return r

Как видите, этот вариант bracket практически не отличается от старого, только его тело «обернуто» в функцию mask и принимает в качестве аргумента некую функцию restore. Семантика всего этого хозяйства следующая. Внутри кода, обернутого в mask, все асинхронные исключения маскируются, то есть, откладываются на потом. Чтобы временно восстановить прежний режим маскировки, находясь внутри mask, код нужно обернуть в переданную в качестве аргумента функцию restore. Обратите внимание, что restore не отключает маскировку, а только возвращает ее к состоянию, имевшему место перед вызовом mask. Таким образом, вызывая mask, мы можем быть уверены, что вложенные функции не отключат маскировку. Потоки, посылающие асинхронные исключения с помощью throwTo потокам с замаскированными асинхронными исключениями, блокируются до тех пор, пока асинхронные исключения в целевом потоке не будут размаскированы. Таким образом, в критических участках кода мы можем либо вовсе игнорировать асинхронные исключения, либо обрабатывать их только там, где мы к этому готовы.

К сожалению, написанное выше является правдой только наполовину, на самом деле все несколько сложнее. В Haskell есть небольшое множество так называемых прерываемых операций (interruptible operations). К ним, например, относятся функции takeMVar, openFile и другие. Во время вызова таких функций поток может получить асинхронное исключение даже если асинхронные исключения замаскированы. Как ни странно, в большинстве случаев нам с вами хотелось бы именно такого поведения. Иначе takeMVar и прочие функции могли бы навсегда заблокировать поток. Если вы уверены, что в вашем случае это никогда не произойдет, то можете совсем запретить асинхронные исключения, используя функцию uninterruptibleMask вместо mask. Но делать так считается грязным хаком и не рекомендуется.

Что еще нужно знать об асинхронных исключениях и их маскировке? Не так уж много. Во-первых, при использовании функций из семейства catch в обработчиках исключений используется установленный ранее режим маскировки. Таким образом, если поток получит два асинхронных исключения, второе не выкинет нас из обработчика исключений. Обратите внимание, что это не относится к функциям семейства try (о них чуть ниже), поскольку фактически они не делают никакой обработки, а только возвращают результат. Во-вторых, нити, создаваемые с помощью forkIO, наследуют режим маскировки от родительской нити. Например, вы можете сказать mask $ _ -> forkIO, чтобы асинхронные исключения в дочернем потоке были замаскированы сразу, а не спустя какое-то время после его создания. Если такое поведение forkIO является нежелательным, используйте функцию forkIOUnmasked. Наконец, в-третьих, для отладки удобно использовать функцию getMaskingState, возвращающую текущее состояние маскировки.

Все это, конечно, может звучать немного запутанно, но хорошая новость заключается в том, что напрямую работать с асинхронными исключениями приходится крайне редко. Haskell поощряет написание чистого кода, в котором про асинхронные исключения можно забыть. Большинство функций, всякие там bracket и modifyMVar, а также все функции, работающие с Handle, являются безопасными в плане асинхронных исключений «из коробки». Когда же вы используете STM, все операции выполняются атомарно. При возникновении исключений, в том числе асинхронных, вся транзакция откатывается, благодаря чему данные остаются в непротиворечивом состоянии. Наконец, если по каким-то причинам вы пишите сложный код, работающий с кучей всяких там MVar, ничто не мешает просто обернуть этот код в mask.

Разумеется, хаскелисты придумали все эти заумные асинхронные исключения не просто так. Благодаря асинхронным исключениям у потоков почти всегда есть шанс прибраться за собой. Любой код, даже написанный кем-то другим, всегда прерываем. Но самое главное, с помощью асинхронных исключений можно легко и непринужденно прерывать операции по таймауту при помощи throwTo и делать многие другие полезные вещи. Никого же не смущают сигналы в Erlang’е, а ведь асинхронные исключения — это ничто иное, как их аналог.

Заключение

Нельзя не отметить, что есть много других функций для работы с исключениями: assert, try, tryJust, catchJust, handleJust, catches… Например, последняя позволяет устанавливать сразу несколько обработчиков исключений, а не один. Внутри монады STM исключения следует бросать при помощи функции throwSTM, а ловить — при помощи catchSTM. Но пользы от последней довольно мало, так как все изменения, произведенные в транзакции, откатываются перед вызовом обработчика исключений. Ну и не слишком спешите применять полученные знания на практике, ведь IO (Maybe a) тоже неплохо работает.

Дополнительные материалы:

  • Лучше всего про исключения в Haskell написано в восьмой и девятой главах книги «Parallel and Concurrent Programming in Haskell»;
  • Описание многих тонких моментов можно найти в документации к модулю Control.Exception;
  • Обратите пристальное внимание на пакет errors, он заслуживает быть использованным в любом более-менее серьезном проекте на Haskell;
  • Занятный обзор способов обработки ошибок в Haskell;
  • Michael Snoyman о том, как поймать вообще все исключения;

Как обычно, замечания, дополнения и прочего рода комментарии приветствуются!

Дополнение: В GHC 7.8 немного изменилась обработка исключений, появился тип SomeAsyncException.

Метки: Haskell, Функциональное программирование.

Понравилась статья? Поделить с друзьями:
  • Throw er unhandled error event node js
  • Through trial and error перевод
  • Throttling error esme has exceeded allowed message limits
  • Threed32 ocx ошибка
  • Three phase current error value перевод