【问题标题】:Should Maybe's be used to hold error messages?是否应该使用 Maybe 来保存错误消息?
【发布时间】:2017-08-31 08:48:49
【问题描述】:

我有一个接受用户输入的 Haskell 函数和另一个验证该输入的函数。当然,验证可能会失败,在这种情况下,我想返回一条错误消息,提供一些关于错误操作的反馈。

我知道有很多方法可以做到这一点。经过我的一点经验,似乎最好的方法是使用Either String a。让我失望的是我不在乎a。要么失败,我想存储更多信息,要么成功。 a 被浪费了。

使用Maybe String 存储错误消息是一种可接受的方式吗? 我感觉有点倒退,但是完全忽略Either 右侧的值也感觉很糟糕。这里的规范是什么?

【问题讨论】:

  • 听起来您正在编写的代码包含一些反模式(在这种情况下,可能是副作用代码)。您通常应该关心Either 值的either 值(没有双关语或混淆意图)——也就是说,在LeftRight 的情况下都应该得到适当的处理。
  • @Jules 我同意我应该关心Either 的任一侧,这就是为什么我认为使用Maybe 是要走的路。你是说用户输入是反模式吗?
  • 从语义上讲,您的验证函数可能会返回错误消息,或者不返回错误消息,因此使用Maybe 作为其返回类型看起来完全合理。这不是Either 通常暗示的“错误消息”与“正确结果”模式。

标签: haskell types algebraic-data-types


【解决方案1】:

出于以下几个原因,我鼓励使用Except String ()(或Either String ())而不是Maybe String

  • 稍后您可能会发现,您的验证函数可以方便地返回数据结构的某些部分。例如,在验证 String 是电话号码时,您可能希望返回区号、号码的第一部分和第二部分,并提供类似 String -> Except String (Int, Int, Int) 或类似的验证类型。使不返回任何有趣内容的验证器具有 Foo -> Except String () 类型,这使得它们只是这种模式的一个特例——因此更容易组合在一起。
  • 继续“配合”部分,您稍后可能会发现您想用较小的验证器构建一个大型验证器。也许你有一个验证器来检查一个人是否指定了有效的年龄和出生日期,并希望以此构建一个验证器来检查年龄是否与出生日期相符。 EitherMonad 实例将在这里有所帮助;例如:

    validatePerson p now = do
        age <- validateAge p
        date <- validateBirthdate p
        validateMatchingAgeAndDate age date now
    

    或者也许有两种方法可以正确验证某个值,而您想要允许其中任何一种。那么bigValidator v = option1 v &lt;|&gt; option2 v 是一种将两种验证方式结合起来的廉价而愉快的方式。

    作为附带的好处,这些将现有验证器组合成更大验证器的方法将立即被其他 Haskeller 识别。

  • 有一个非常强烈的约定,Nothing 是失败的。使用相反的约定不一定是问题,但可能会让其他贡献者感到困惑,并可能在很远的将来让你自己感到困惑。

【讨论】:

  • 在使用了几个月的haskell之后,我想说我非常同意这是要走的路。
  • Either String User 不是更好的选择吗?
  • @mb14 什么是User,为什么您认为它比() 更好?
  • 我错过了并认为 OP 想要验证用户,因此是一个函数 validateUser :: User -&gt; Error String User。是有链的优点。但是,它不会强制验证函数不修改输入。
【解决方案2】:

什么是错误消息,什么是预期值只是您的观点。如果您不关心 a 结果值但关心可能的错误消息,那么就您而言,该消息就是您感兴趣的值。因此,请确保您可以将其存储为 Maybe String

事实上,Either 几乎没有什么不同。我向后发现Either 通常被认为是“可能失败的类型”。是的,它的 monad 实例恰好以使 Left error-ish 和 Right success-ish 的方式工作,但从头算起 Either 只是一个简单的双函子,表示两种类型的总和。事实上,如果 Haskell 一直都有类型运算符,那么 Either a b 可能会写成 a + ba || b

如果你有一个Either String a 并且想要“没收”可能的a 值,最简单的方法是在它上面加上fmap (const ()),得到一个Either String (),它与Maybe String 同构,但是“看起来更像 String 有错误字符”,尽管正如我所说,这有点傻。

为了从您所说的错误消息的类型中清楚地看出,我既不会使用Either 也不会使用Maybe,而是使用Except String ()。通常,无论如何,错误值都会被其他一些单子捕获,所以你会有例如ExceptT String IO ().

【讨论】:

    【解决方案3】:

    我建议使用与Maybe String 同构的自定义类型。

    data Result = OK | Error String
    

    甚至

    newtype Result = Result (Maybe String)
    

    后者可以避免重复Maybe 实例,因为我们可以利用GeneralizedNewtypeDeriving 来获得相同的效果

    newtype Result = Result (Maybe String)
       deriving (Eq, Show) -- etc.
    

    (更新:这可能没那么有用,因为大多数此类是标准类,无论如何都可以自动派生。正如亚历克西斯·金(Alexis King)在下面指出的那样,不能将其变成应用程序/替代/单子,因为例如,因为种类不匹配。)

    现在的主要缺点是必须使用两个构造函数:

    foo :: Result -> ...
    foo (Result (Just x)) = ...
    foo (Result (Nothing)) = ...
    

    这很无聊。可以更进一步定义pattern synonyms

    pattern OK      = Result Nothing
    pattern Error x = Result (Just x)
    

    这样我们就可以假装使用上面显示的第一个 data 定义而不是 newtype

    foo :: Result -> ...
    foo (Error x) = ...
    foo OK        = ...
    

    根据您是否需要Result 类型上的这些实例,这可能是合理的或有点矫枉过正。即使您不这样做,这也不是一个很长的编写方法,而且我认为许多(大多数?)Haskellers 也会对这两个必需的扩展感到满意。

    对于它的价值,在阅读了下面的 cmets 之后,目前我认为第一个 data Result 方法是最好的。这很简单,很清楚地说明了这一点,如果需要一些类实例,您可能希望手动定义它们(或自动派生它们,对于标准 Prelude 类)。

    【讨论】:

    • 使用Maybe,验证器可以简单地与&lt;|&gt;链接;对于这种自定义类型,情况并非如此。
    • 我刚刚了解了一个新的类型类,感谢您提供有关&lt;|&gt; 的注释。使 Result 成为 Alternative 的实例来获得这种行为是否合理?或者如果我自己实现它,我可能做错了?
    • @jcolemang 是的,这是合理的。不得不复制已经存在的代码有点不幸。我会用另一种方法更新。
    • 关键是使用&lt;|&gt; 链接验证器的整个前提(假设这意味着您要检查它们是否都接受输入为有效)是倒退和混乱的,因为a &lt;|&gt; b 是全部当ab 成功时。因此命名为Alternative。链接验证器的正确方法是使用&gt;&gt;(或者现在该运营商的流行名称)。例如,这适用于Either String ()
    • 此类型不能是FunctorAlternativeApplicativeMonad,因为它的类型错误,所以您的 GND 示例不正确。不过,这并没有那么糟糕,因为这些实例并没有真正的意义——假设Error 应该短路,就没有地方保存“成功”值,而且通常的类型类都没有用处。
    【解决方案4】:

    是的,Maybe String 没问题(它的形状精确映射到您的函数范围),但它不会很好地组合,因为所有有用的实例都采用与 Maybe 相反的语义。 Either String () 会更有用(就其 monad/Applicative 实例而言),也会更清晰。

    但是有一个更合适的“验证应用”抽象值得探索,它允许您链接验证并累积错误(即不会在第一个错误上短路)。一些实现风格在validation package 中。来自文档:

    >>> _Success # (+1) <*> _Success # 7 :: AccValidation String Int
    AccSuccess 8
    
    >>> _Failure # ["f1"] <*> _Success # 7 :: AccValidation [String] Int
    AccFailure ["f1"]
    
    >>> _Success # (+1) <*> _Failure # ["f2"] :: AccValidation [String] Int
    AccFailure ["f2"]
    
    >>> _Failure # ["f1"] <*> _Failure # ["f2"] :: AccValidation [String] Int
    AccFailure ["f1","f2"]
    

    请注意,AccValidation m 不是有效的Monad。当组合的解析器/验证应用程序突然需要使用绑定时,我经历了一些轻微的痛苦。

    【讨论】:

      【解决方案5】:

      注意:我不认为这通常是最好的解决方案,但考虑到这是一个只有我将从事的任务,并且不包括对好的 Haskell 的任何要求实践(或Haskell),这对我来说是最简单的解决方案。我从chi的回答中借鉴了一些想法。


      我想要Maybe 的确切功能,但仍想避免返回Nothing 表示成功,而Just msg 表示失败。为了解决这个问题,我创建了 Maybe 及其数据构造函数的真正别名:

      {-# LANGUAGE PatternSynonyms #-}
      
      type Result = Maybe
      pattern AllGood = Nothing
      pattern Fail x = Just x
      
      -- example usage
      isValidBorrower :: [String] -> Result String
      isValidBorrower args
        | length args /= 3 = Fail "Wrong number of arguments"
        | otherwise = AllGood
      

      这允许Maybe 的完整功能,但如果您的类型不正确并且您没有在类型签名中看到Maybe String,类型检查器仍将显示MaybeFailure,这会产生误导。

      缺点是这两个功能仍然有效:

      foo :: Maybe String
      foo = AllGood
      
      bar :: Result String
      bar = Nothing
      

      【讨论】:

        猜你喜欢
        • 2023-03-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-03
        • 2021-04-07
        • 2017-07-21
        • 1970-01-01
        相关资源
        最近更新 更多