Лучше использовать монаду ошибок с валидацией в ваших монадических функциях или реализовать собственную монаду с валидацией прямо в вашем bind?

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кода ошибка или ситуация, которая обычно может возникнуть (например, кто-то вводит неправильный ввод в ваше приложение). Если это ошибка, я бы постарался сделать так, чтобы ее никогда не было (например, с помощью умных конструкторов или создания более сложных типов).

Если это допустимый сценарий, то некоторая обработка ошибок во время выполнения является подходящей. Тогда я спрашиваю: что для меня действительно означает, что a 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.
    

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

  2. Если вы откладываете валидацию до самого конца и используете Right ...везде, вам больше не нужна монада. Вы можете просто сделать чистые вычисления и устранить любые возможные ошибки в конце. ИМХО, этот подход очень рискованный, так как неверное пользовательское значение может привести к тому, что в другом месте будут неверные данные, потому что вы не достаточно быстро остановили вычисления. И, если случится так, что какой-то другой метод обновит пользователя, чтобы он снова стал действительным, вы в конечном итоге получите где-то недопустимые данные и даже не будете знать об этом.

  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, и теперь я вижу, как ... негибким был бы такой подход, так что большое спасибо за то, что выправили меня!
Джимми Хоффа