Может быть, монада против исключений

34

Интересно, в чем преимущества Maybe монады перед исключениями? Похоже, Maybeэто просто явный (и довольно трудоемкий) способ try..catchсинтаксиса.

обновление Пожалуйста, обратите внимание, что я намеренно не упоминаю Haskell.

Владимир
источник

Ответы:

57

Использование Maybe(или его двоюродного брата, Eitherкоторый работает в основном таким же образом, но позволяет вам возвратить произвольное значение вместо Nothing), служит несколько иной цели, чем исключения. В терминах Java это похоже на наличие проверенного исключения, а не исключения во время выполнения. Он представляет собой нечто ожидаемое, с которым вам приходится иметь дело, а не ошибку, которую вы не ожидали.

Таким образом, функция like indexOfбудет возвращать Maybeзначение, потому что вы ожидаете, что элемент отсутствует в списке. Это очень похоже на возврат nullиз функции, за исключением безопасного типа, который заставляет вас иметь дело с nullделом. Eitherработает так же, за исключением того, что вы можете вернуть информацию, связанную с регистром ошибки, так что на самом деле это больше похоже на исключение, чем Maybe.

Так в чем же преимущества подхода Maybe/ Either? С одной стороны, это первоклассный гражданин языка. Давайте сравним функцию, использующую функцию, Eitherгенерирующую исключение. В исключительном случае единственным реальным выходом из ситуации является try...catchутверждение. Для этой Eitherфункции вы можете использовать существующие комбинаторы, чтобы сделать управление потоком более понятным. Вот пара примеров:

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

firstThing <|> secondThing <|> throwError (SomeError "error message")

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

do a <- getA
   b <- getB
   optional (optimize query)
   execute query a b

Оба эти случая яснее и короче, чем использование try..catch, и, что более важно, более семантические. Использование функции типа <|>или optionalделает ваши намерения намного понятнее, чем использование try...catchдля обработки исключений.

Также обратите внимание, что вам не нужно засорять ваш код такими строками if a == Nothing then Nothing else ...! Весь смысл лечения Maybeи Eitherкак монады состоит в том, чтобы избежать этого. Вы можете закодировать семантику распространения в функцию bind, так что вы бесплатно получаете проверки null / error. Единственный раз, когда вам нужно явно проверить, это если вы хотите вернуть что-то отличное от Nothingданного Nothing, и даже тогда это легко: есть множество стандартных библиотечных функций, которые делают этот код более приятным.

Наконец, еще одним преимуществом является то, что Maybe/ Eitherтип просто проще. Нет необходимости расширять язык дополнительными ключевыми словами или структурами управления - все это просто библиотека. Поскольку они являются просто обычными значениями, это упрощает систему типов - в Java вы должны различать типы (например, тип возвращаемого значения) и эффекты (например, throwsоператоры), которые вы бы не использовали Maybe. Они также ведут себя так же, как и любой другой определенный пользователем тип - нет необходимости иметь специальный код обработки ошибок, запеченный в языке.

Еще одна победа заключается в том, что Maybe/ Eitherявляются функторами и монадами, что означает, что они могут использовать существующие функции управления потоком монад (которых достаточно) и, в общем, хорошо играть вместе с другими монадами.

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

Другая проблема заключается в возвращении нескольких типов ошибок (это относится только к Either). За исключением исключений, вы можете создавать любые разные типы исключений в одной и той же функции. с Either, вы получите только один тип. Это можно преодолеть с помощью подтипа или ADT, содержащего все различные типы ошибок в качестве конструкторов (этот второй подход обычно используется в Haskell).

Тем не менее, в целом, я предпочитаю подход Maybe/, Eitherпотому что считаю его более простым и более гибким.

Тихон Джелвис
источник
В основном согласны, но я не думаю, что «необязательный» пример имеет смысл - кажется, что вы можете сделать это также с исключениями: void Optional (Action act) {try {act (); } catch (Exception) {}}
Errorsatz
11
  1. Исключение может содержать больше информации об источнике проблемы. OpenFile()может бросить FileNotFoundили NoPermissionи TooManyDescriptorsт. д. Ни один не несет эту информацию.
  2. Исключения могут использоваться в контекстах, в которых отсутствуют возвращаемые значения (например, с конструкторами в языках, в которых они есть).
  3. Исключение позволяет вам очень легко отправлять информацию в стек без большого количества if None return Noneинструкций -style.
  4. Обработка исключений почти всегда оказывает большее влияние на производительность, чем просто возвращение значения.
  5. Самое главное, что исключение и монада Maybe имеют разные цели - исключение используется для обозначения проблемы, в то время как Maybe - нет.

    «Медсестра, если в комнате 5 есть пациент, можете ли вы попросить его подождать?»

    • Может быть, монада: «Доктор, в 5-й комнате нет пациента».
    • Исключение: «Доктор, комнаты 5 нет!»

    (обратите внимание на «если» - это означает, что врач ожидает монаду «Может быть»)

дуб
источник
7
Я не согласен с пунктами 2 и 3. В частности, 2 на самом деле не создает проблемы в языках, которые используют монады, потому что эти языки имеют соглашения, которые не создают загадку необходимости обрабатывать ошибки во время построения. Что касается 3, языки с монадами имеют соответствующий синтаксис для обработки монад прозрачным способом, который не требует явной проверки (например, Noneзначения могут быть просто переданы). Ваш пункт 5 является лишь верным ... вопрос в том, какие ситуации однозначно исключительны? Как оказалось… не много .
Конрад Рудольф
3
Что касается (2), вы можете написать bindтак, чтобы тестирование Noneне вызывало синтаксических издержек. Очень простой пример, C # просто соответствующим образом перегружает Nullableоператоры. Не Noneтребуется проверка даже при использовании типа. Конечно, проверка все еще выполняется (она безопасна для типов), но негласно и не загромождает ваш код. То же самое относится в некотором смысле к вашему возражению против моего возражения против (5), но я согласен, что это не всегда применимо.
Конрад Рудольф
4
@Oak: Относительно (3), весь смысл обращения Maybeс монадой состоит в том, чтобы сделать распространение Noneнеявным. Это означает, что если вы хотите вернуть Noneданные None, вам вообще не нужно писать какой-либо специальный код. Единственный раз, когда вам нужно соответствовать, если вы хотите сделать что-то особенное None. Вам никогда не нужны if None then Noneтакие заявления.
Тихон Джелвис
5
@ Oak: это правда, но это не совсем моя точка зрения. То, что я говорю, это то, что вы nullточно так же проверяете (например if Nothing then Nothing) бесплатно, потому что Maybeэто монада. Это закодировано в определении bind ( >>=) для Maybe.
Тихон Джелвис
2
Кроме того, относительно (1): вы могли бы легко написать монаду, которая может нести информацию об ошибках (например Either), которая ведет себя точно так же Maybe. Переключение между ними на самом деле довольно просто, потому что Maybeна самом деле это просто особый случай Either. (В Haskell, вы можете думать , Maybeкак Either ().)
Тихон Jelvis
6

«Возможно» не является заменой исключений. Исключения предназначены для использования в исключительных случаях (например: открытие соединения с БД, а сервер БД отсутствует, хотя и должно быть). «Возможно» - для моделирования ситуации, когда вы можете иметь или не иметь действительное значение; скажем, вы получаете значение из словаря для ключа: оно может быть там или может не быть - нет ничего «исключительного» в любом из этих результатов.

Неманья Трифунович
источник
2
На самом деле, у меня есть довольно много кода со словарями, где отсутствующий ключ означает, что в какой-то момент все пошло ужасно неправильно, скорее всего, это ошибка в моем коде или в коде, который преобразовал ввод в то, что используется в этой точке.
3
@delnan: Я не говорю, что отсутствующее значение никогда не может быть признаком исключительного состояния - просто не должно быть. Возможно, действительно моделирует ситуацию, когда переменная может иметь или не иметь допустимое значение - представьте, что в C # могут быть типы, допускающие значения NULL.
Неманя Трифунович
6

Я второй ответ Тихона, но я думаю, что есть очень важный практический момент, который все упускают:

  1. Механизмы обработки исключений, по крайней мере на основных языках, тесно связаны с отдельными потоками.
  2. EitherМеханизм не соединен с резьбой на всех.

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

Концепция обещаний позволяет вам писать асинхронный код, подобный этому (взят из последней ссылки):

var greetingPromise = sayHello();
greetingPromise
    .then(addExclamation)
    .then(function (greeting) {
        console.log(greeting);    // 'hello world!!!!’
    }, function(error) {
        console.error('uh oh: ', error);   // 'uh oh: something bad happened’
    });

По сути, обещание - это объект, который:

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

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

sacundim
источник
Это не имеет ничего общего с потоками. JavaScript в браузере всегда запускается в одном потоке, а не в нескольких. Но вы все равно не можете использовать исключение, потому что вы не знаете, когда ваша функция будет вызвана в будущем. Асинхронный не означает автоматически участие потоков. Это также причина, почему вы не можете работать с исключениями. Вы можете получить исключение, только если вы вызываете функцию, и она сразу же выполняется. Но вся цель асинхронного заключается в том, что он запускается в будущем, часто когда что-то другое завершается, а не сразу. Вот почему вы не можете использовать исключения там.
Дэвид Рааб
1

Система типов Haskell потребует от пользователя подтверждения возможности a Nothing, в то время как языки программирования часто не требуют, чтобы исключение было перехвачено. Это означает, что во время компиляции мы узнаем, что пользователь проверил наличие ошибки.

chrisaycock
источник
1
В Java есть проверенные исключения. И люди часто относились даже к ним плохо.
Владимир
1
@ DairT'arg ButJava требует проверки на ошибки ввода-вывода (в качестве примера), но не на NPE. В Haskell все наоборот: проверяется нулевой эквивалент (возможно), но ошибки ввода-вывода просто генерируют (не проверяются) исключения. Я не уверен, насколько это важно, но я не слышу, чтобы Haskellers жаловались на Maybe, и я думаю, что многие люди хотели бы, чтобы проверяли NPE.
1
@delnan Люди хотят проверить NPE? Они должны быть сумасшедшими. И я имею в виду это. Делать проверенный NPE имеет смысл, только если у вас есть способ избежать его в первую очередь (с помощью необнуляемых ссылок, которых нет в Java).
Конрад Рудольф
5
@KonradRudolph Да, люди, вероятно, не хотят, чтобы его проверяли в том смысле, что им придется добавлять a throws NPEк каждой сигнатуре и catch(...) {throw ...} к каждому телу метода. Но я верю, что существует рынок для check в том же смысле, что и с Maybe: nullability является необязательной и отслеживается в системе типов.
1
@delnan А, тогда я согласен.
Конрад Рудольф
0

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

Telastyn
источник
8
Ну, это не имеет те же недостатки , так как он может быть статически тип проверяется при правильном использовании. Не существует эквивалента исключения нулевого указателя при использовании, возможно, монад (опять же, при условии, что они используются правильно).
Конрад Рудольф
Верно. Как вы думаете, что следует уточнить?
Теластин
@Konrad Это потому, что для использования значения (возможно) в Maybe вы должны проверить None (хотя, конечно, есть возможность автоматизировать большую часть этой проверки). В других языках подобные вещи хранятся вручную (в основном по философским причинам, я считаю).
Донал Феллоуз
2
@Donal Да, но обратите внимание, что большинство языков, которые реализуют монады, предоставляют соответствующий синтаксический сахар, так что эту проверку часто можно полностью скрыть. Например, вы можете просто добавить два Maybeчисла, написав a + b без необходимости проверки None, и результат снова является необязательным значением.
Конрад Рудольф
2
Сравнение с обнуляемыми типами справедливо для Maybe типа , но использование Maybeв качестве монады добавляет синтаксический сахар, который позволяет выразить нуль-логику намного элегантнее.
tdammers
0

Обработка исключений может быть реальной болью для факторинга и тестирования. Я знаю, что python предоставляет хороший синтаксис «with», который позволяет перехватывать исключения без жесткого блока «try ... catch». Но, например, в Java попробуйте блоки catch, которые являются большими, шаблонными, либо многословными, либо чрезвычайно многословными, и их трудно разбить. Вдобавок к этому Java добавляет весь шум вокруг проверенных и непроверенных исключений.

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

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

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

обкрадывать
источник