Haskell подходит к обработке ошибок - PullRequest
40 голосов
/ 30 июня 2011

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

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

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

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

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

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

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

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

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

Ответы [ 3 ]

32 голосов
/ 30 июня 2011

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

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

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

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

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


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

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

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

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

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


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

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

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

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

26 голосов
/ 30 июня 2011

Давайте посмотрим, что вы хотите сделать:

Убедитесь, что указаны имена файлов

А если нет? Просто уйти, верно?

Убедитесь, что файлы доступны и доступны для чтения

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

Убедитесь, что файлы имеют действительные заголовки

А если нет? Та же проблема - пропустить плохих, прервать раньше, предупредить о плохих и т. Д ...

Обработка файлов, ошибки при разборе ошибок, инвариантные ошибки и т. Д.

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

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

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

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

7 голосов
/ 30 июня 2011

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

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

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

нет ничего плохого
func x = case tryEval x of
             Left e -> Left e
             Right val -> Right $ val + 1

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...