我有一个程序专门用于创建“学生”(名字、姓氏、年龄)并验证输入数据。我的问题是:当我插入学生(例如姓名没有 2 个字母或年龄低于 18 岁)时,程序仅显示一个错误。如何使用“任一函数”创建包含所有错误的字符串?
module Student where
data Student = Student {firstName::FirstName, lastName::LastName, age::Age}
deriving Show
newtype FirstName = FirstName String
deriving Show
newtype LastName = LastName String
deriving Show
newtype Age = Age Int
deriving Show
mkStudent :: String -> String -> String -> Either String Student
mkStudent fn ln a =
Student <$> validate fn
<*> validate ln
<*> validate a
aceptableLetters = ['a'..'z']++['A'..'Z']
validateFn :: String -> Either String FirstName
validateFn fn
| length fn < 2 = Left "First name has to at least 2 letters"
| length fn > 100 = Left "First name is limited to 100 characters"
| not . null $ filter (\c -> not . elem c $ aceptableLetters) fn = Left "First name contains unacceptable chars"
| otherwise = Right . FirstName $ fn
validateLn :: String -> Either String LastName
validateLn lastName
| length lastName < 2 = Left "Last name has to at least 2 letters"
| length lastName > 100 = Left "Last name is limited to 100 characters"
| not . null $ filter (\c -> not . elem c $ aceptableLetters) lastName = Left "Last name contains unacceptable chars"
| otherwise = Right . LastName $ lastName
validateA :: String -> Either String Age
validateA a
| age <= 18 = Left "Student has to be at least 18"
| age > 100 = Left "Student has more than 100 years. Probably it is an error."
| otherwise = Right . Age $ age
where
age = read a
class Validate v where
validate :: String -> Either String v
instance Validate FirstName where
validate=validateFn
instance Validate LastName where
validate=validateLn
instance Validate Age where
validate=validateA
Monad 和 Applicative 实例都不能累积错误。任何这样做的实现都会违反单子法则。它们必须停在第一个 Left 值处。因此,如果你想使用 Either 来累积错误,你必须手动完成,而不是通过 Applicative 或 Monad 实例。
您正在寻找的是验证。这样,你就可以写:
causes :: Applicative f => Bool -> a -> Validation (f a) ()
True `causes` err = Failure $ pure err
False `causes` err = Success ()
validateA :: String -> Validation [String] Age
validateA a = (Success . Age $ age)
<* (age <= 18) `causes` "Student has to be at least 18"
<* (age > 100) `causes` "Student has more than 100 years. Probably it is an error."
where age = read a
对于其他验证者也是如此。
mkStudent
仍然如您所写:应用组合器是组合验证值的正确方法。
一个简单的解决方法是将左侧
Either
更改为自定义数据类型。我的意思是,您可以拥有 Either String Age
,而不是 Either ErrorType Age
,然后定义
data ErrorType = LowAge | WrongName
在我看来,这也是更好的编程实践,因为您可以将错误抛出及其处理程序分开。特别是在 Haskell 和函数式编程中,我不会使用字符串表示来处理错误。然后,您可以将错误合并到一个列表中,即:
mkStudent :: String -> String -> String -> Either [ErrorType] Student
然后只需使用另一个函数来处理该列表并将其打印给用户,或记录它,或者您想要用它做的任何事情。
一个基本方法是定义一个自定义帮助函数,用于收集列表中的错误:
collectError :: Error e a -> [e]
collectError (Left e) = [e]
collectError _ = [] -- no errors
然后,我们可以如下利用该助手:
mkStudent :: String -> String -> String -> Either [String] Student
mkStudent fn ln a = case (validate fn, validate ln, validate a) of
(Right xfn, Right xln, Right xa) -> Right (Student xfn xln xa)
(efn , eln , ea ) ->
Left (collectError efn ++ collectError eln ++ collectError ea)
这可能不是最优雅的方式,但很简单。
如果此模式在程序中经常使用,我会很想制作一个自定义的
Applicative
来记录所有错误。类似的东西
data Result e a = Error e | OK a
deriving Functor
instance Semigroup e => Applicative (Result e) where
pure = OK
(OK f) <*> (OK x) = OK $ f x
(Error e1) <*> (Error e2) = Error (e1 <> e2)
(Error e1) <*> _ = Error e1
_ <*> (Error e2) = Error e2
(这应该已经以某个名称存在于库中。)
那么,
mkStudent :: String -> String -> String -> Result [String] Student
mkStudent fn ln a =
Student <$> validate fn
<*> validate ln
<*> validate a
应该可以正常工作,前提是验证函数在失败时返回
Error ["message"]
,返回类型为 Result [String] T
。