Самый чистый способ сообщить об ошибках в Haskell

22

Я работаю над изучением Haskell, и я столкнулся с тремя различными способами устранения ошибок в функциях, которые я пишу:

  1. Я могу просто написать error "Some error message.", что выдает исключение.
  2. Я могу вернуть свою функцию Maybe SomeType, где я могу или не могу вернуть то, что хотел бы вернуть.
  3. Я могу вернуть свою функцию Either String SomeType, где я могу вернуть либо сообщение об ошибке, либо то, что мне было предложено вернуть в первую очередь.

У меня вопрос: какой метод устранения ошибок я должен использовать и почему? Может быть, я должен по-разному, в зависимости от контекста?

Мое текущее понимание:

  • «Исключительно сложно иметь дело с исключениями в чисто функциональном коде, и в Haskell хочется, чтобы все было как можно более чисто функциональным».
  • Возврат Maybe SomeType- это то, что нужно сделать, если функция либо потерпит неудачу, либо преуспеет (т. Е. Не существует разных способов ее сбоя ).
  • Возвращаясь Either String SomeTypeявляется правильным , что нужно сделать , если функция может потерпеть неудачу в любом из различных способов.
CmdrMoozy
источник

Ответы:

32

Хорошо, первое правило обработки ошибок в Haskell: никогда не используйтеerror .

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

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

Теперь вопрос становится Maybeпротив Either. Maybeхорошо подходит для чего-то подобного head, которое может возвращать или не возвращать значение, но есть только одна возможная причина для сбоя. Nothingговорит что-то вроде «это сломалось, и вы уже знаете, почему». Некоторые скажут, что это указывает на частичную функцию.

Самая надежная форма обработки ошибок - Either+ ошибка ADT.

Например, в одном из моих компиляторов хобби у меня есть что-то вроде

data CompilerError = ParserError ParserError
                   | TCError TCError
                   ...
                   | ImpossibleError String

data ParserError = ParserError (Int, Int) String
data TCError = CouldntUnify Ty Ty
             | MissingDefinition Name
             | InfiniteType Ty
             ...

type ErrorM m = ExceptT CompilerError m -- from MTL

Теперь я определяю кучу типов ошибок, вкладывая их так, чтобы у меня была одна великолепная ошибка верхнего уровня. Это может быть ошибка на любом этапе компиляции или ImpossibleErrorошибка, которая означает ошибку компилятора.

Каждый из этих типов ошибок старается как можно дольше хранить информацию для красивой печати или другого анализа. Еще важнее то, что, не имея строки, я могу проверить, что при запуске некорректной программы через средство проверки типов действительно возникает ошибка объединения! Если что-то есть String, оно исчезает навсегда, и любая содержащаяся в нем информация непрозрачна для компилятора / тестов, так что просто Either Stringне слишком хороша.

Наконец, я упаковываю этот тип в ExceptTновый монадный трансформатор от MTL. Это, по сути, EitherTи поставляется с отличным набором функций для чистого и приятного вывода ошибок.

Наконец, стоит упомянуть, что на Haskell есть механизмы для поддержки обработки исключений, как это делают другие языки, за исключением того, что перехват исключения находится в IO. Я знаю, что некоторые люди любят использовать их для IOтяжелых приложений, где все может потерпеть неудачу, но так редко, что им не нравится думать об этом. Используете ли вы эти нечистые исключения или просто ExceptT Error IOвопрос вкуса. Лично я выбираю, ExceptTпотому что мне нравится напоминать о вероятности неудачи.


Как итог,

  • Maybe - Я могу потерпеть неудачу одним очевидным способом
  • Either CustomType - Я могу потерпеть неудачу, и я скажу тебе, что случилось
  • IO+ исключения - я иногда терплю неудачу. Проверьте мои документы, чтобы увидеть, что я бросаю, когда я делаю
  • error - Я тоже тебя ненавижу, пользователь
Даниэль Гратцер
источник
Этот ответ многое проясняет для меня. Я догадывался, основываясь на том факте, что встроенные функции любят headили, lastкажется, используют error, поэтому мне было интересно, действительно ли это хороший способ сделать что-то, и я просто что-то упустил. Это отвечает на этот вопрос. :)
CmdrMoozy
5
@CmdrMoozy рад помочь :) К сожалению, прелюдия имеет такие плохие практики. Таков путь наследства: /
Даниэль Гратцер
3
+1 Ваше резюме потрясающе
recursion.ninja
2

Я бы не стал разделять 2 и 3, основываясь на том, сколько способов что-то может потерпеть неудачу. Как только вы думаете, что есть только один возможный путь, другой покажет свое лицо. Вместо этого метрикой должно быть «заботятся ли мои абоненты о том, почему что-то не получилось?

Кроме того, мне не сразу понятно, что Either String SomeTypeможет привести к ошибке. Я бы сделал простой алгебраический тип данных с условиями с более наглядным именем.

То, что вы используете, зависит от характера проблемы, с которой вы сталкиваетесь, и от особенностей программного пакета, с которым вы работаете. Хотя я хотел бы избежать # 1.

Telastyn
источник
На самом деле, использование Eitherдля ошибок является очень известным паттерном в Haskell.
Rufflewind
1
@rufflewind - Eitherне непонятная часть, использование Stringв качестве ошибки, а не фактической строки.
Теластин
Ах, я неправильно понял ваше намерение.
Rufflewind