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

Правильный дизайн для обработки исключений Haskell

В настоящее время я пытаюсь обратить внимание на правильный способ использования исключений в Haskell. Как работают исключения, достаточно прямолинейно; Я пытаюсь получить четкое представление о правильном способе их интерпретации.

Основная позиция заключается в том, что в хорошо разработанном приложении исключения не должны выходить на верхний уровень. Любое исключение явно является тем, что дизайнер не ожидал - то есть ошибка программы (например, деление на ноль), а не необычное время выполнения (например, файл не найден).

С этой целью я написал простой обработчик исключений верхнего уровня, который улавливает все исключения и печатает сообщение stderr, говорящее "это ошибка" (до повторного исключения исключения для завершения программы).

Однако предположим, что пользователь нажимает Ctrl + C. Это вызывает исключение. Очевидно, что это не какая-то ошибка программы. Однако неспособность предвидеть и реагировать на прерывание пользователя, такое как это, может считаться ошибкой. Поэтому, возможно, программа должна поймать это и обработать его надлежащим образом, сделав необходимую очистку перед выходом.

Дело в том, что... Код, который обрабатывает это, поймает исключение, освободит любые ресурсы или что-то еще, а затем убьет исключение! Поэтому, если исключение делает его на верхнем уровне, это не обязательно означает, что он был необработанным. Это просто означает, что мы хотели быстро выйти.

Итак, мой вопрос: должны ли исключения использоваться для контроля потока таким образом? Должна ли каждая функция, которая явно ловит UserInterrupt, использовать явные конструкции управления потоком, чтобы выходить из нее вручную, а не восстанавливать исключение? (Но как узнать, как вызывающий может выйти?) Возможно ли, чтобы UserInterrupt достиг верхнего уровня? Но в этом случае, нормально ли для ThreadKilled тоже по тому же аргументу?

Короче говоря, должен ли обработчик прерывания сделать специальный случай для UserInterrupt (и, возможно, ThreadKilled)? Как насчет HeapOverflow или StackOverflow? Это ошибка? Или это "обстоятельство, выходящее за рамки управления программой"?

4b9b3361

Ответ 1

Очистка при наличии исключений

Однако, неспособность предвидеть и реагировать на прерывание пользователя, такое как это, может считаться ошибкой. Поэтому, возможно, программа должна поймать это и обработать его надлежащим образом, сделав необходимую очистку перед выходом.

В некотором смысле вы правы - программист должен предвидеть исключения. Но не поймать их. Вместо этого вы должны использовать исключительные функции, такие как bracket. Например:

import Control.Exception

data Resource

acquireResource :: IO Resource
releaseResource :: Resource -> IO ()

workWithResource = bracket acquireResource releaseResource $ \resource -> ...

Таким образом, ресурсы будут очищены независимо от того, будет ли программа отменена с помощью Ctrl + C.

Если исключения превышают верхний уровень?

Теперь я хотел бы обратиться к другому вашему заявлению:

Основная позиция заключается в том, что в хорошо продуманном приложении исключения не должны выходить на верхний уровень.

Я бы сказал, что в хорошо продуманном приложении исключения - прекрасный способ прервать. Если с этим возникают какие-либо проблемы, вы делаете что-то неправильно (например, хотите выполнить действие очистки в конце main), но это должно быть сделано в bracket!).

Вот что я часто делаю в своих программах:

  • Определите тип данных, который представляет любую возможную ошибку - все, что может пойти не так. Некоторые из них часто переносят другие исключения.

    data ProgramError
      = InputFileNotFound FilePath IOException
      | ParseError FilePath String
      | ...
    
  • Определите, как печатать ошибки удобным для пользователя способом:

    instance Show ProgramError where
      show (InputFileNotFound path e) = printf "File '%s' could not be read: %s" path (show e)
      ...
    
  • Объявить тип как исключение:

    instance Exception ProgramError
    
  • Бросьте эти исключения в программу всякий раз, когда мне это нравится.

Должны ли я перехватывать исключения?

Исключения, которые вы ожидаете, должны быть пойманы и завернуты (например, в InputFileNotFound), чтобы дать им больше контекста. Как насчет исключений, которые вы не ожидаете?

Я вижу некоторое значение при печати "это ошибка" для пользователей, так что они сообщают о проблеме вам. Если вы это сделаете, вы должны ожидать UserInterrupt - это не ошибка, как вы говорите. Как вы должны относиться к ThreadKilled, зависит от вашего приложения - буквально, ожидаете ли вы этого?

Это, однако, ортогонально "хорошему дизайну" и больше зависит от того, на каких пользователях вы нацеливаетесь, что вы ожидаете от них и чего они ожидают от вашей программы. Ответ может варьироваться от просто распечатки исключения до диалога, в котором говорится: "Нам очень жаль, вы хотели бы представить отчет разработчикам?".

Ответ 2

Должны ли исключения использоваться для управления потоком таким образом?

Да. Я настоятельно рекомендую вам прочитать разрыв из цикла, в котором показано, как Either и EitherT по своему ядру не более чем абстракции для выхода из код рано. Исключения являются лишь особым случаем такого поведения, когда вы выходите из-за ошибки, но нет причин, почему это должен быть единственный случай, когда вы выходите преждевременно.