EdZhavoronkov
EdZhavoronkov

Reputation: 187

Using ErrorT in Haskell

I have a task to implement a function that repeatly asks user for a password and says if it is not correct. If the password is OK, then it says "Storing in database and exits". I need to use ErrorT monad transformer to tell user, that his password is incorrect, so the basic behavior looks like this

GHCi>runErrorT askPassword'
Enter your new password:
qwerty
Incorrect input: password is too short!
qwertyuiop
Incorrect input: password must contain some digits!
qwertyuiop123
Incorrect input: password must contain some punctuations!
qwertyuiop123!!!
Storing in database...
GHCi>

askPassword' is like

data PwdError = PwdError String
type PwdErrorMonad = ErrorT PwdError IO

instance Error PwdError where
    noMsg = PwdError "Unknown error"
    strMsg s = PwdError s 

instance Show PwdError where
    show (PwdError s) = show s 

askPassword' :: PwdErrorMonad ()
askPassword' = do
    liftIO $ putStrLn "Enter your new password:"
    value <- msum $ repeat getValidPassword'
    liftIO $ putStrLn "Storing in database..." 

and getValidPassword looks like this:

getValidPassword' :: PwdErrorMonad String
getValidPassword' = do
    s <- liftIO getLine
    if length s < 8
        then throwError $ PwdError "Incorrect input: password is too short!"
        else if (not $ any isNumber s)
            then throwError $ PwdError "Incorrect input: password must contain some digits!"
            else if (not $ any isPunctuation s)
                then throwError $ PwdError "Incorrect input: password must contain some punctuations!"
                else return s

What am i missing here? Thanks for help!

Upvotes: 0

Views: 377

Answers (1)

user2407038
user2407038

Reputation: 14588

The ErrorT datatype records errors in the Left field, which holds exactly one error. It cannot save each error. If you want to just "throw an error", use the IO monad and use throw/catch from Control.Exception.

If you want to use your current code, try the following:

withErr :: MonadError e m => (e -> m ()) -> m a -> m a
withErr f ac = catchError ac (\e -> f e >> throwError e)

This function applies an additional function to the error before it is rethrown. In your case, you just want to print the error:

askPassword' :: PwdErrorMonad ()
askPassword' = do
    liftIO $ putStrLn "Enter your new password:" 
    _ <- msum $ repeat $ withErr (liftIO . print) getValidPassword'
    liftIO $ putStrLn "Storing in database..." 

Another approach is to use the mtl API to create a function which runs a function until it doesn't return an error.

try :: MonadError e m => (e -> m ()) -> m a -> m a
try h ac = go where go = catchError ac (\e -> h e >> go)

You can do anything with the errors, for example, collect them in a list:

recordErrors :: MonadError e m => m a -> m (a, [e])
recordErrors = runWriterT . try (tell . (:[])) . lift 

However, what you want is to just print them:

askPassword' :: PwdErrorMonad ()
askPassword' = do
    liftIO $ putStrLn "Enter your new password:"
    _ <- try (liftIO . print) getValidPassword'  
    liftIO $ putStrLn "Storing in database..." 

Upvotes: 1

Related Questions