在数据验证中使用“任一”

问题描述 投票:0回答:3

我有一个程序专门用于创建“学生”(名字、姓氏、年龄)并验证输入数据。我的问题是:当我插入学生(例如姓名没有 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
haskell
3个回答
10
投票

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
仍然如您所写:应用组合器是组合验证值的正确方法。


0
投票

一个简单的解决方法是将左侧

Either
更改为自定义数据类型。我的意思是,您可以拥有
Either String Age
,而不是
Either ErrorType Age
,然后定义

data ErrorType = LowAge | WrongName

在我看来,这也是更好的编程实践,因为您可以将错误抛出及其处理程序分开。特别是在 Haskell 和函数式编程中,我不会使用字符串表示来处理错误。然后,您可以将错误合并到一个列表中,即:

mkStudent :: String -> String -> String -> Either [ErrorType] Student

然后只需使用另一个函数来处理该列表并将其打印给用户,或记录它,或者您想要用它做的任何事情。


0
投票

一个基本方法是定义一个自定义帮助函数,用于收集列表中的错误:

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

© www.soinside.com 2019 - 2024. All rights reserved.