最好在monadic函数中使用带验证的错误monad,还是直接在绑定中实现带有验证的自己的monad?


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)

我可以通过添加项目或商店等方式来实现monadic函数来转换用户,但是最终我可能得到一个无效的用户,因此这些monadic函数将需要验证他们获得和/或创建的用户。

所以,我应该:

  • 将其包装在错误的monad中,并使monadic函数执行验证
  • 将其包装在错误的monad中,并让使用者按引发适当错误响应的顺序绑定一个monadic验证函数(以便他们可以选择不验证并携带无效的用户对象)
  • 实际上将其构建到User上的绑定实例中,从而有效地创建了我自己的错误monad类型,它会自动对每个绑定执行验证

我可以看到对这三种方法的正面和负面的看法,但我想知道社区对此情况最常做的事情。

所以在代码方面,选项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"]

Answers:


5

拳头我会问自己:一个无效User的代码错误或一种通常会发生的情况(例如,有人向您的应用程序输入了错误的输入)。如果是错误,我将尝试确保它永远不会发生(例如使用智能构造函数或创建更复杂的类型)。

如果是有效方案,则在运行时进行一些错误处理是适当的。然后,我会问:这是什么真正意味着我,一个User无效的

  1. 这是否意味着无效User代码会使某些代码失败?您的代码部分是否依赖于a User始终有效的事实?
  2. 还是只是意味着它的不一致性需要稍后修复,但在计算过程中不会破坏任何内容?

如果为1.,我肯定会选择某种错误的monad(标准错误或您自己的错误),否则您将无法保证代码正常运行。

创建自己的monad或使用一堆monad变压器是另一个问题,也许这会有所帮助:有人在野外遇到过Monad Transformer吗?


更新:查看您的扩展选项:

  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 ...在所有地方使用,则您不再需要monad。您可以只进行纯计算,最后解决所有可能的错误。恕我直言,这种方法非常冒险,因为无效的用户值可能会导致其他地方的数据无效,因为您没有足够快地停止计算。而且,如果碰巧有其他方法更新了用户,使它再次有效,您最终将在某处拥有无效数据,甚至不知道该数据。

  3. 这里有几个问题。

    • 最重要的是,monad必须接受任何类型参数,而不仅仅是User。因此,您validate必须具有对的u -> ValidUser u任何类型的类型u。因此,不可能编写这样的monad来验证的输入return,因为它return必须是完全多态的。
    • 接下来,我不了解您是否case return u of在的定义中进行匹配>>=。的重点ValidUser应该是区分有效值和无效值,因此monad必须确保这始终是正确的。所以这可能很简单

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

    这看起来已经很像了Either

通常,仅在以下情况下才使用自定义单子

  • 没有现有的monad可为您提供所需的功能。现有的monad通常具有许多支持功能,更重要的是,它们具有monad转换器,因此您可以将它们组合成monad堆栈。
  • 或者,如果您需要一个过于复杂而无法描述为monad堆栈的monad。

您的最后两点非常宝贵,我没有考虑过!绝对是我一直在寻找的智慧,感谢分享您的想法,我绝对会和#1一起去的!
Jimmy Hoffa 2013年

昨晚只是将整个模块捆绑在一起,您就没事了。我将我的validate方法放入进行了所有模型更新的少量键组合器中,实际上,这样做更加有意义。我确实本来打算排在#3之后,现在我知道这种方法是多么的僵硬,所以非常感谢您让我挺直!
Jimmy Hoffa
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.