1
votes

Utilisation de «l'un ou l'autre» dans la validation des données

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


0 commentaires

3 Réponses :


0
votes

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


0 commentaires

0
votes

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.


0 commentaires

9
votes

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.


0 commentaires