Краще використовувати монаду помилок з валідацією у своїх монадійних функціях, або реалізувати власну монаду з валідацією безпосередньо у вашому зв’язку?


9

Мені цікаво, який дизайн краще для зручності користування / ремонту, а що краще для співтовариства.

З огляду на модель даних:

type Name = String

data Amount = Out | Some | Enough | Plenty deriving (Show, Eq)
data Container = Container Name deriving (Show, Eq)
data Category = Category Name deriving (Show, Eq)
data Store = Store Name [Category] deriving (Show, Eq)
data Item = Item Name Container Category Amount Store deriving Show
instance Eq (Item) where
  (==) i1 i2 = (getItemName i1) == (getItemName i2)

data User = User Name [Container] [Category] [Store] [Item] deriving Show
instance Eq (User) where
  (==) u1 u2 = (getName u1) == (getName u2)

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

Отже, я повинен просто:

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

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

Тож у кодовому плані щось подібне, варіант 1:

addStore s (User n1 c1 c2 s1 i1) = validate $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]

варіант 2:

addStore s (User n1 c1 c2 s1 i1) = Right $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ Right someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"] >>= validate
-- in this choice, the validation could be pushed off to last possible moment (like inside updateUsersTable before db gets updated)

варіант 3:

data ValidUser u = ValidUser u | InvalidUser u
instance Monad ValidUser where
    (>>=) (ValidUser u) f = case return u of (ValidUser x) -> return f x; (InvalidUser y) -> return y
    (>>=) (InvalidUser u) f = InvalidUser u
    return u = validate u

addStore (Store s, User u, ValidUser vu) => s -> u -> vu
addStore s (User n1 c1 c2 s1 i1) = return $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someValidUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]

Відповіді:


5

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

Якщо це правильний сценарій, то під час обробки під час виконання деяких помилок підходить обробка помилок. Тоді я запитую: Що це дійсно означає для мене , що Userє недійсним ?

  1. Це означає, що недійсний Userможе зробити якийсь код невдалим? Чи покладаються частини вашого коду на те, що а Userзавжди діє?
  2. Або це просто означає, що його непослідовність, яку потрібно виправити пізніше, але нічого не порушує під час обчислення?

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

Створення власної монади або використання стека монад-трансформаторів - це ще одне питання, можливо, це буде корисним: хтось коли-небудь стикався з трансформатором монади в дикій природі? .


Оновлення: переглядаючи розширені параметри:

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

    user :: ... -> Either YourErrorType User
    -- more generic:
    user :: (MonadError YourErrorType m) ... -> m User
    -- Or if you actually don't need to differentiate errors:
    user :: ... -> Maybe User
    -- or more generic:
    user :: (MonadPlus m) ... -> m User
    -- etc.
    

    Багато бібліотек беруть подібну appropach, наприклад Map, Setабо Seqприховати основну реалізацію , так що це не можливо , щоб створити структуру , яка не підкоряється їх інваріантів.

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

  3. Тут є кілька проблем.

    • Найголовніше - монада повинна приймати будь-який параметр типу, а не лише User. Таким чином, ви validateповинні мати тип u -> ValidUser uбез будь-яких обмежень на u. Тому неможливо написати таку монаду, яка підтверджує входи return, тому що вона returnповинна бути повністю поліморфною.
    • Далі, я не розумію, що ви відповідаєте за case return u ofвизначенням >>=. Основним моментом ValidUserмає бути розмежування дійсних та недійсних значень, і тому монада повинна гарантувати, що це завжди відповідає дійсності. Так що це може бути просто

      (>>=) (ValidUser u) f = f u
      (>>=) (InvalidUser u) f = InvalidUser u
      

    І це вже дуже схоже Either.

Як правило, я б користувався користувальницькою монадою, лише якщо

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

Ваші останні два моменти безцінні, і я не думав про них! Однозначно мудрість, яку я шукав, дякую, що поділився своїми думками, я обов'язково поїду з №1!
Джиммі Хоффа

Просто минулий вечір я зв'язав цілий модуль, і ти просто мертвий. Я вивів свій метод перевірки у невелику кількість ключових комбінаторів, у яких я робив усі оновлення моделі, і це насправді має набагато більше сенсу, як це. Я дійсно збирався йти після №3, і тепер я бачу, наскільки… негнучким був би такий підхід, тож дякую тоні за те, що мене поставили прямо!
Джиммі Хоффа
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.