Подтвердить что ты не робот

Как правильно использовать Control.Exception.catch в Haskell?

Может кто-нибудь объяснит разницу между поведением в ghci следующих строк:

catch (return $ head []) $ \(e :: SomeException) -> return "good message"

возвращает

"*** Exception: Prelude.head: empty list

но

catch (print $ head []) $ \(e :: SomeException) -> print "good message"

возвращает

"good message"

Почему первый случай не поймал исключение? Почему они разные? И почему первый случай ставит двойную кавычку перед сообщением об исключении?

Спасибо.

4b9b3361

Ответ 1

Рассмотрим, что происходит в первом случае:

catch (return $ head []) $ \(e :: SomeException) -> return "good message"

Вы создаете thunk head [], который return ed как действие IO. Этот thunk не вызывает никакого исключения, потому что он не оценивается, поэтому весь вызов catch (return $ head []) $ ... (который имеет тип IO String) создает String thunk без исключения. Исключение происходит только тогда, когда ghci пытается распечатать результат позже. Если вы попробовали

catch (return $ head []) $ \(e :: SomeException) -> return "good message"
    >> return ()

вместо этого исключение не было бы напечатано.

Это также причина, по которой вы получаете _ "* Исключение: Prelude.head: empty list_. GHCi начинает печатать строку, которая начинается с ". Затем он пытается оценить строку, что приводит к исключению, и это распечатывается.

Попробуйте заменить return на evaluate (который заставляет свой аргумент WHNF) в качестве

catch (evaluate $ head []) $ \(e :: SomeException) -> return "good message"

то вы заставите thunk оценивать внутри catch, который выдает исключение и позволяет обработчику перехватить его.

В другом случае

catch (print $ head []) $ \(e :: SomeException) -> print "good message"

исключение возникает внутри части catch, когда print пытается исследовать head [], и поэтому он улавливается обработчиком.


Обновление:. Как вы полагаете, хорошо, чтобы заставить значение, предпочтительно, его полную нормальную форму. Таким образом, вы гарантируете, что вас не ждут "неожиданные сюрпризы" в ленивых трюках. В любом случае это хорошо, например, вы можете столкнуться с трудностями при поиске, если ваш поток возвращает неоцененный thunk, и он фактически оценивается в другом, ничего не подозревающем потоке.

Модуль Control.Exception уже имеет evaluate, что заставляет thunk в его WHNF. Мы можем легко увеличить его, чтобы заставить его полностью заполнять NF:

import Control.DeepSeq
import Control.Seq
import Control.Exception
import Control.Monad

toNF :: (NFData a) => a -> IO a
toNF = evaluate . withStrategy rdeepseq

Используя это, мы можем создать строгий вариант catch, который заставляет данное действие его NF:

strictCatch :: (NFData a, Exception e) => IO a -> (e -> IO a) -> IO a
strictCatch = catch . (toNF =<<)

Таким образом, мы уверены, что возвращаемое значение будет полностью оценено, поэтому мы не получим никаких исключений при его изучении. Вы можете проверить, что если вы используете strictCatch вместо catch в первом примере, он работает как ожидалось.

Ответ 2

return $ head []

wraps head [] в действии ввода-вывода (потому что catch имеет тип IO, иначе это будет любая монада) и возвращает его. Нет ничего пойманного, потому что нет ошибки. head [] сам не оценивается в этой точке, благодаря ленивости, но только возвращается. Таким образом, return добавляет только слой обертки, а результат всего вашего выражения catch head [], вполне допустимый, неоценимый. Только тогда, когда GHCi или ваша программа на самом деле попытаются использовать это значение в какой-то более поздней точке, оно будет оценено, а пустая ошибка списка будет выбрана - в другой точке.

print $ head []

с другой стороны, немедленно оценивает head [], что дает ошибку, которая впоследствии попадает.

Вы также можете увидеть разницу в GHCi:

Prelude> :t head []
head [] :: a

Prelude> :t return $ head []
return $ head [] :: Monad m => m a

Prelude> :t print $ head []
print $ head [] :: IO ()

Prelude> return $ head [] -- no error here!

Prelude> print $ head []
*** Exception: Prelude.head: empty list   

Чтобы этого избежать, вы можете просто заставить значение:

Prelude> let x = head [] in x `seq` return x
*** Exception: Prelude.head: empty list

Ответ 3

GHCi работает в монаде IO и return для IO не форсирует свой аргумент. Таким образом, return $ head [] не вызывает никаких исключений, и исключение, которое не выбрасывается, невозможно поймать. Затем печать результата вызывает исключение, но это исключение больше не входит в область catch.

Ответ 4

Тип IO a состоит из двух частей:

  • Структура, часть, которая выходит и выполняет побочные эффекты. Это представлено IO.
  • Результат, чистое значение, содержащееся внутри. Это представлено a.

Функция catch выполняет только исключения при прохождении структуры IO.

В первом примере вы вызываете print для значения. Поскольку для печати значения требуется выполнение ввода-вывода с ним, любые исключения, возникающие в пределах значения, оказываются в самой структуре. Итак, catch перехватывает исключение, и все хорошо.

С другой стороны, return не проверяет свой аргумент. На самом деле, вы гарантированы законами монады, что вызов return не влияет на структуру вообще. Таким образом, ваше значение просто проходит прямо.

Итак, если на ваш код не влияет исключение, то откуда появляется сообщение об ошибке? Ответ, что удивительно, вне вашего кода. GHCi неявно пытается передать print каждое переданное ему выражение. Но к тому времени мы уже выходим за пределы catch, следовательно, вы видите сообщение об ошибке.