应该使用Maybe来保存错误消息吗?

时间:2017-04-05 16:02:55

标签: haskell types algebraic-data-types

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

我知道有很多方法可以做到这一点。在我获得的小经验之后,似乎最好的方法是使用Either String a。什么让我失望的是我不关心a。它失败了,我想存储更多信息,或者它成功。 a被浪费了。

使用Maybe String是一种可接受的方式来存储错误消息吗?这让我觉得倒退,但完全无视Either右边的值感觉非常糟糕太。这里的规范是什么?

5 个答案:

答案 0 :(得分:10)

我鼓励Except String ()(或Either String ())使用Maybe String,原因如下:

  • 稍后您可能会发现验证功能可以方便地返回数据结构的某些部分。例如,在验证String是电话号码时,您可能希望返回区号,第一部分和第二部分,提供类似String -> Except String (Int, Int, Int)或类似的验证类型。让那些没有返回任何有趣内容的验证器具有类型Foo -> Except String (),这使得它们只是这种模式的一个特例 - 因此更容易组合在一起。
  • 继续"融合在一起"部分,您可能稍后发现您想要从较小的验证器中构造一个大验证器。也许你有一个验证器,可以检查一个人是否指定了一个有效的年龄和出生日期,并且想要建立一个验证器,同时检查年龄是否适合出生日期。 Monad的{​​{1}}个实例会对此有所帮助;例如:

    Either

    或许有两种方法可以使某些值正确验证,并且您希望允许其中任何一种。然后validatePerson p now = do age <- validateAge p date <- validateBirthdate p validateMatchingAgeAndDate age date now 是一种结合两种验证方式的廉价而愉快的方式。

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

  • 有一个非常强烈的约定,bigValidator v = option1 v <|> option2 v是失败的。使用相反的约定不一定是问题,但可能会让其他贡献者感到困惑,并且可能在将来对你自己造成困扰。

答案 1 :(得分:4)

什么是错误信息以及预期值是什么只是你的观点。如果您不关心a结果值但是关心可能的错误消息,那么就您所关注的那样,该消息是感兴趣的价值。因此,请务必将其存储为Maybe String

事实上它与Either略有不同。我后来发现的是Either通常被认为是“可能失败的类型”。是的,它的monad实例恰好以Left error-ish和Right成功的方式工作,但ab initio Either只是一个简单的bifunctor,表示两种类型的总和。实际上,如果Haskell一直有类型操作符,Either a b可能会写成a + ba || b

如果您有Either String a并想要“没收”可能的a值,最简单的方法就是fmap (const ())覆盖它,从而产生Either String (),这与Maybe String同构,但“看起来更像String有错误字符”,尽管我说这有点傻。

要从您正在讨论错误消息的类型中明确说明,我不会使用EitherMaybe,而是使用Except String ()。通常,错误值无论如何都会被其他monad捕获,因此您可以使用例如ExceptT String IO ()

答案 2 :(得分:4)

我建议使用自定义类型,与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在下面指出的那样,不能将它变成一个applicative / alternative / monad,for例如,因为种类不匹配。)

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

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类型的实例,这可能是合理的或有点矫枉过正。即使你不这样做,也不是一个很长的写法,我认为很多(大多数?)Haskeller也会对这两个必需的扩展感到满意。

在阅读下面的评论之后,我认为第一个data Result方法是最好的。它很简单,非常清楚,如果需要某些类实例,您可能需要手动定义它们(或者为标准Prelude类自动派生它们。)

答案 3 :(得分:3)

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

但是有一个更合适的&#34;验证应用程序&#34;值得探索的抽象,它允许您链接验证并累积错误(即不会在第一个错误上发生短路)。 SOY的实施风格在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。当组合的解析器/验证应用程序突然需要使用bind时,我经历了一些轻微的痛苦。

答案 4 :(得分:1)

注意:我不认为这是一般的最佳解决方案,但考虑到这只是我将要处理的任务,并且不包括任何好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的完整功能,但如果您的类型不正确且类型签名中未显示MaybeFailure,则类型检查器仍会显示Maybe String误导。

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

foo :: Maybe String
foo = AllGood

bar :: Result String
bar = Nothing