J'ai un programme dédié à la création de "Student" (prénom, nom, âge) et à la validation des données d'entrée. Mon problème est que: lorsque j'insère un étudiant dont, par exemple, le nom n'a pas 2 lettres ou l'âge est inférieur à 18 ans - le programme n'affiche qu'une seule erreur. Comment utiliser "L'une ou l'autre fonction" pour créer par exemple une chaîne contenant toutes les erreurs?
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
3 Réponses :
Un moyen de base serait de définir une fonction d'assistance personnalisée qui collecte les erreurs dans une liste:
mkStudent :: String -> String -> String -> Result [String] Student
mkStudent fn ln a =
Student <$> validate fn
<*> validate ln
<*> validate a
Ensuite, nous pouvons exploiter l'aide comme suit:
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
Ce n'est probablement pas la manière la plus élégante, mais c'est simple.
Si ce modèle est fréquemment utilisé dans un programme, je serais tenté de créer un Applicatif personnalisé qui enregistre toutes les erreurs. Quelque chose comme
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)
(Cela devrait déjà exister dans les bibliothèques sous un nom.)
Ensuite,
collectError :: Error e a -> [e] collectError (Left e) = [e] collectError _ = [] -- no errors
Une solution simple serait de changer le Soit de gauche en un type de données personnalisé. Ce que je veux dire, c'est qu'au lieu d'avoir Either String Age , vous pourriez avoir Either ErrorType Age , puis définir
mkStudent :: String -> String -> String -> Either [ErrorType] Student
Dans mon opinion que ce serait également une meilleure pratique de programmation car vous sépareriez le lancement d'erreur et son gestionnaire. Surtout dans Haskell et la programmation fonctionnelle, je n'emporterais pas de représentations de chaîne pour gérer les erreurs. Ensuite, vous pouvez par exemple combiner les erreurs dans une liste, c'est-à-dire:
data ErrorType = LowAge | WrongName
Ensuite, il suffit d'avoir une autre fonction pour gérer la liste et l'imprimer à l'utilisateur, ou la consigner, ou tout ce que vous voudriez en faire.
Les instances Monad et Applicative de l'une ou l'autre ne peuvent accumuler d'erreurs: selon la loi, elles doivent s'arrêter à la première valeur Left. Donc, si vous souhaitez utiliser Either pour accumuler des erreurs, vous devez le faire à la main, et non via les instances Applicative ou Monad.
Ce que vous recherchez à la place est Validation . Avec cela, vous pourriez écrire:
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
et de même pour vos autres validateurs. mkStudent reste tel que vous l'avez écrit: les combinateurs applicatifs sont le bon moyen de combiner les valeurs de validation.