Можливо, монада проти винятків


Відповіді:


57

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

Таким чином, така функція, як 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як монада - це уникати цього. Ви можете кодувати семантику розповсюдження у функції прив'язки, щоб ви отримали перевірку нуля / помилки безкоштовно. Єдиний раз, коли вам доведеться чітко перевірити, - якщо ви хочете повернути щось інше, ніж Nothingдане a Nothing, і навіть тоді це легко: існує маса стандартних функцій бібліотеки, щоб зробити цей код приємнішим.

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

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

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

Інша проблема полягає в тому, що повернення декількох типів помилок (це стосується лише Either). За винятком, ви можете кидати будь-які різні типи винятків в одній і тій же функції. з Either, ви отримуєте лише один тип. Це можна подолати за допомогою підтипу або ADT, що містить усі різні типи помилок як конструктори (цей другий підхід - це те, що зазвичай використовується в Haskell).

І все-таки я більше віддаю перевагу Maybe/ Eitherпідходу, оскільки вважаю його більш простим та гнучким.


Здебільшого погоджено, але я не вважаю, що "необов'язковий" приклад має сенс - здається, ви можете це зробити так само добре, як і за винятками: недійсний Необов'язковий (Дія дії) {try {act (); } лов (виняток) {}}
Errorsatz

11
  1. Виняток може містити більше інформації про джерело проблеми. OpenFile()може кинути FileNotFoundабо NoPermissionабо і TooManyDescriptorsт.д. Ні не несе цю інформацію.
  2. Винятки можуть бути використані в контекстах, у яких відсутні значення повернення (наприклад, з конструкторами на мовах, які їх мають).
  3. Виняток дозволяє дуже легко надсилати інформацію в стек, без великої кількості if None return Noneвисловлювань -style.
  4. Поводження з винятками майже завжди має більший вплив на продуктивність, ніж просто повернення значення.
  5. Найголовніше, що виняток і монада "Можливо" мають різні цілі - виняток використовується для позначення проблеми, а "Можливо".

    "Медсестра, якщо у кімнаті 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

"Можливо" не є заміною винятків. Винятки мають бути використані у виняткових випадках (наприклад: відкриття db-з'єднання та db-сервера немає, хоча це має бути). "Можливо" - це для моделювання ситуації, коли у вас може бути або не бути дійсного значення; скажімо, ви отримуєте значення зі словника для ключа: воно може бути там, а може бути і немає - немає нічого "виняткового" в будь-якому з цих результатів.


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

3
@delnan: Я не кажу, що відсутнє значення ніколи не може бути ознакою виняткового стану - цього просто не повинно бути. Можливо, дійсно моделюється ситуація, коли змінна може мати, а може не мати дійсне значення - подумайте про нульові типи в C #.
Неманья Трифунович

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. Дозволяє підключити обробник несправності, який буде викликаний, якщо обчислення обіцянки не вдалося. Якщо обробника немає, помилка поширюється на більш пізні обробники в ланцюзі.

В основному, оскільки підтримка рідного винятку мови не працює, коли обчислення відбувається в декількох потоках, реалізація обіцянок повинна забезпечити механізм керування помилками, і вони виявляються монадами, схожими на Haskell Maybe/ Eitherтипи.


У ній немає нічого спільного з нитками. JavaScript у браузері завжди працює в одному потоці, а не в декількох потоках. Але ви все одно не можете використовувати виняток, оскільки не знаєте, коли ваша функція буде викликана в майбутньому. Асинхронність не означає автоматично залучення ниток. Це також причина, чому ви не можете працювати з винятками. Виняток ви можете отримати лише в тому випадку, якщо ви зателефонуєте до функції і негайно вона буде виконуватися. Але вся мета Асинхронного полягає в тому, що він працює в майбутньому, часто, коли щось інше закінчено, і не відразу. Тому ви не можете використовувати винятки там.
Девід Рааб

1

Система типу Haskell вимагає від користувача визнати можливість a Nothing, тоді як мови програмування часто не вимагають, щоб вилучення було виключено. Це означає, що під час компіляції ми будемо знати, що користувач перевірив помилку.


1
У Java є перевірені винятки. І люди часто погано ставилися до них.
Володимир

1
@ DairT'arg ButJava вимагає перевірки на помилки вводу-виводу (як приклад), але не на NPE. У Haskell навпаки: нульовий еквівалент (Можливо) перевіряється, але помилки вводу-виводу просто викидають (неперевірені) винятки. Я не впевнений, наскільки це різниця, але я не чую, як Haskellers скаржиться на Можливо, і я думаю, що багато людей хотіли б перевірити NPE.

1
@delnan Люди хочуть перевірити NPE? Вони повинні бути з розуму. І я маю на увазі це. Перевірка NPE має сенс, лише якщо у вас є спосіб уникнути її в першу чергу (через відсутність нульових посилань, яких у Java немає).
Конрад Рудольф

5
@KonradRudolph Так, люди, ймовірно, не хочуть, щоб його перевіряли в тому сенсі, що їм доведеться додавати "" " throws NPEдо кожного підпису та catch(...) {throw ...} до кожного окремого методу. Але я вважаю, що існує ринок перевірених в тому ж сенсі, що і у Можливо: зведена нульовість є необов'язковою і відслідковується в системі типів.

1
@delnan Ах, тоді я згоден.
Конрад Рудольф

0

Монада, можливо, є такою ж, як і використання більшості мов основної мови для перевірки "null означає помилку" (за винятком випадків, коли вона вимагає перевірки нуля), і має в основному ті ж переваги та недоліки.


8
Ну, це не має тих самих недоліків, оскільки при правильному використанні його можна перевірити статично. Немає еквівалента виключення нульового вказівника при використанні, можливо, монад (знову ж таки, припускаючи, що вони використовуються правильно).
Конрад Рудольф

Справді. Як ви вважаєте, що це слід уточнити?
Теластин

@Konrad Це тому, що для використання значення (можливо) в "Можливо", ви повинні перевірити "Немає" (хоча, звичайно, є можливість автоматизувати багато перевірки). Інші мови зберігають таке посібник (переважно, з філософських причин).
Стипендіати Дональда

2
@Donal Так, але зауважте, що більшість мов, які реалізують монади, забезпечують відповідний синтаксичний цукор, так що цю перевірку часто можна приховати повністю. Наприклад, ви можете просто додати два Maybeчисла, записавши a + b без необхідності перевірки None, і результат знову є необов'язковим значенням.
Конрад Рудольф

2
Порівняння з нульовими типами справедливо для Maybe типу , але використання Maybeяк монади додає синтаксичний цукор, що дозволяє виражати нульову логіку набагато більш елегантно.
tdammers

0

Поводження з винятками може бути справжнім болем для факторингу та тестування. Я знаю, що python забезпечує гарний синтаксис "з", що дозволяє відловлювати винятки без жорсткого блоку "спробувати ... зловити". Але в Java, наприклад, спробуйте ловити блоки великі, шаблонні, або багатослівні, або надзвичайно багатослівні, і їх важко розбити. На додаток до цього, Java додає весь шум навколо перевірених та неперевірених винятків.

Якщо замість цього ваша монада ловить винятки і розглядає їх як властивість монадичного простору (замість аномалії обробки), то ви можете змішувати та співставляти функції, які ви прив'язуєте до цього простору незалежно від того, що вони кидають чи ловлять.

Якщо ще краще, ваша монада перешкоджає умовам, коли можуть статися винятки (наприклад, натискання нульової перевірки на "Можливо"), то ще краще. якщо ... то набагато, набагато простіше піддавати тестування та тестування, ніж спробувати ... зловити.

З того, що я бачив, Go застосовує аналогічний підхід, уточнюючи, що кожна функція повертається (відповідь, помилка). Це на зразок того, як "підняти" функцію в простір монади, де тип відповіді в ядрі прикрашений вказівкою про помилку, і фактично викидання та ловлення винятків у бік кроку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.