我有一个Haskell函数,它接受用户输入和另一个验证此输入的函数。当然,验证可能会失败,在这种情况下,我想返回一条错误消息,提供有关错误操作的反馈。
我知道有很多方法可以做到这一点。在我获得的小经验之后,似乎最好的方法是使用Either String a
。什么让我失望的是我不关心a
。它失败了,我想存储更多信息,或者它成功。 a
被浪费了。
使用Maybe String
是一种可接受的方式来存储错误消息吗?这让我觉得倒退,但完全无视Either
右边的值感觉非常糟糕太。这里的规范是什么?
答案 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 + b
或a || b
。
如果您有Either String a
并想要“没收”可能的a
值,最简单的方法就是fmap (const ())
覆盖它,从而产生Either String ()
,这与Maybe String
同构,但“看起来更像String
有错误字符”,尽管我说这有点傻。
要从您正在讨论错误消息的类型中明确说明,我不会使用Either
或Maybe
,而是使用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