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

Подходы Haskell к обработке ошибок

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

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

Скажем, я бы хотел написать инструмент командной строки, который обрабатывает файлы, переданные в командной строке. Я бы хотел:

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

Итак, довольно простой инструмент для обработки файлов.

В Haskell я бы обернул этот код в какую-то комбинацию монадов, используя Maybe и Lither, и при необходимости переведя и распространяя ошибки. В конце концов, все это доходит до монады IO, где я могу выводить статус пользователю.

На другом языке я просто бросаю исключение и улавливаю в соответствующем месте. Непосредственная. Я не трачу много времени на познавательную неопределенность, пытаясь разгадать, какую комбинацию мне нужно.

Я просто подхожу к этому неправильно или это есть какое-то вещество для этого чувства?

Редактировать: Хорошо, я получаю обратную связь, говорящую мне, что это просто тяжелее, но на самом деле это не так. Итак, вот одна больная точка. В Haskell я занимаюсь стеками монадов, и если мне приходится обрабатывать ошибки, я добавляю еще один слой в этот стек монады. Я не знаю, сколько лифта и другого синтаксического помета мне нужно было добавить, чтобы скомпилировать код, но добавляет нулевой смысловой смысл. Никто не считает, что это добавляет сложности?

4b9b3361

Ответ 1

В Haskell я бы обернул этот код в какую-то комбинацию монадов, используя Maybe и Lither, и при необходимости переведя и распространяя ошибки. В конце концов, все это доходит до монады IO, где я могу выводить статус пользователю.

На другом языке я просто бросаю исключение и улавливаю в соответствующем месте. Непосредственная. Я не трачу много времени на познавательную неопределенность, пытаясь разгадать, какую комбинацию мне нужно.

Я бы не сказал, что вы неизбежно подходите к нему неправильно. Скорее, ваша ошибка заключается в том, что эти два сценария различны; это не так.

"Просто бросить и поймать" эквивалентно тому, чтобы накладывать на всю вашу программу ту же концептуальную структуру, что и некоторая комбинация методов обработки ошибок Haskell. Точная комбинация зависит от систем обработки ошибок на языке, на котором вы сравниваете это, что указывает на то, почему Haskell кажется более сложным: он позволяет смешивать и сопоставлять структуры обработки ошибок на основе необходимости, а не давать вам неявное, одно -size-fits-most solution.

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


В связи с синтаксической неуклюжестью этот неудобный субъект. Теоретически это должно быть безболезненно, но:

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

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

Что касается стеков трансформаторов монады, я думаю, что стандартный подход заключается в newtype весь стек для вашего приложения, получения или реализации экземпляров для соответствующих классов типов (например, MonadError), затем используйте класс типа функций, которые обычно не нуждаются в lift ing. Монадические функции, которые вы пишете для ядра вашего приложения, должны использовать стек newtype d, поэтому также не нужно поднимать. Я думаю, что о единственной вещи с низким смысловым смыслом, которые вы не можете избежать, liftIO.

Работа с большими пакетами трансформаторов может быть фактической головной болью, но только тогда, когда имеется много вложенных слоев разных трансформаторов (накапливаются чередующиеся слои StateT и ErrorT с ContT, посланными посередине, то просто попробуйте сказать мне, что на самом деле сделает ваш код). Это редко то, что вы на самом деле хотите.


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

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

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

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

Ответ 2

Посмотрите на то, что вы хотите сделать:

Подтвердите имена файлов

А если это не так? Просто уходи, правильно?

Проверить файлы доступны и читаемы

А если нет? Обработайте оставшиеся, выбросьте исключение, когда вы нажмете на плохое, предупредите о плохих и обработайте хорошие? Выйти, прежде чем что-либо сделать?

Проверить файлы имеют допустимые заголовки

И если они этого не делают? Тот же вопрос - пропустите плохие, прервите ранний, предупредите о плохих и т.д.

Файлы процессов, ошибки при анализе ошибок, инвариантные ошибки и т.д.

Опять же, и что делать, пропустите неудачные строки, пропустите плохие файлы, прервите, прервите и откат, распечатайте предупреждения, напечатайте настраиваемые уровни предупреждений?

Дело в том, что есть варианты и доступные варианты. Чтобы делать то, что вы хотите, таким образом, чтобы он отражал императивный способ, вам вообще не нужны никакие майфы или эйзеры стеков монады. Все, что вам нужно, это бросать и ловить исключения в IO.

И если вы не хотите использовать исключения во всем мире и получить степень контроля, вы все равно можете сделать это без стеков монады. Например, если вы хотите обработать файлы, которые вы можете получить и получить результаты, и вернуть ошибки в файлы, которые вы не можете, то Эйтерс отлично работает - просто напишите функцию FilePath -> IO (Either String Result). Тогда mapM, что по вашему списку входных файлов. Затем partitionEithers результирующий список, а затем mapM функцию Result -> IO (Maybe String) по результатам и catMaybe строки ошибок. Теперь вы можете mapM print <$> (inputErrors ++ outputErrors) отобразить все ошибки, возникшие в обе фазы.

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

Ответ 3

В чем разница между оценкой Either e a и сопоставлением шаблонов, vs a try и catch, помимо того факта, что она распространяется с исключениями (И если вы используете любую монаду, вы можете имитировать это)

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

Если у вас есть только один возможный сбой, нет ничего плохого в

func x = case tryEval x of
             Left e -> Left e
             Right val -> Right $ val + 1

func x = (+1) <$> trvEval x

Его просто функциональный способ представить одно и то же.