Как правильно использовать 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"
Почему первый случай не поймал исключение? Почему они разные? И почему первый случай ставит двойную кавычку перед сообщением об исключении?
Спасибо.
Ответы
Ответ 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
, следовательно, вы видите сообщение об ошибке.