Epsilon

Handling Multiple Errors in Haskell

Posted on March 17, 2014

There are many ways to validate and sanitize data in Haskell. Recently I had an interesting time with the validation in an application I wrote. In this post I will briefly show how my validation code progressed as I tried adding features to the validation.

Motivation

Since it would require a lot of time to explain my application, let’s use a simple example instead. In your application you have a form that the user needs to fill out before continuing further. The user needs to enter an age and an username to continue further. There are a couple of invariants that should hold with the user input.

If either of these are false, then we need to let the user know that their data is invalid. With this example we will clearly see the issues I ran into and then worked around to a good solution.

Using Maybe

Almost everytime I start going into the unknown world of validation, I start off with the Maybe data type. So let’s see that iteration of the code first.


import Control.Applicative

type Age      = Int
type Username = String
data Form     = Form Age Username deriving (Show)


validateAge :: Age -> Maybe Age
validateAge age
    | age > 0   = Just age
    | otherwise = Nothing


validateUsername :: Username -> Maybe Username
validateUsername username
    | username `elem` usernames = Nothing
    | otherwise                 = Just username
  where
    --for simplicity, lets just use a list as our database of usernames
    usernames = ["foo", "bar", "baz"]


validateForm :: Age -> Username -> Maybe Form
validateForm age username = Form <$> validateAge age
                                 <*> validateUsername username

The age and username validations are simply functions that check the data according to our invariants. They either return Nothing if the data is invalid, or give the data back wrapped in the Just constructor. The form validation uses the Applicative instance of Maybe to hide some of the plumbing of checking for Nothing in the return values of the age and username validation. If either of the two are Nothing then the form validation also returns Nothing.

This validation works, but it is a little crude so far. If we passed the result of validateForm back to the user the only thing they know is that there is a problem with the data they gave us, and nothing more. It would be nice if the user could know which part of the form they did wrong.

Using Either

This is when I moved onto using the Either data type The Either data type works similarly to the Maybe type, but instead of Nothing we can instead pass a different value down. Let’s see what that looks like.


import Control.Applicative

type Age      = Int
type Username = String
type ErrorMsg = String
data Form     = Form Age Username deriving (Show)


validateAge :: Age -> Either ErrorMsg Age
validateAge age
    | age > 0   = Right age
    | otherwise = Left "Age needs to be positive"


validateUsername :: Username -> Either ErrorMsg Username
validateUsername username
    | username `elem` usernames = Left "Username is already taken"
    | otherwise                 = Right username
  where
    --for simplicity, lets just use a list as our database of usernames
    usernames = ["foo", "bar", "baz"]


validateForm :: Age -> Username -> Either ErrorMsg Form
validateForm age username = Form <$> validateAge age
                                 <*> validateUsername username

Functionally the age and username validations are the same as before, but now we pass a String error message when there is an error. The form validation goes through the age validation and then the username validation. If one of them gives an error, the form validation passes that error along. This works great if the user made only one error, as they can immediately fix it and move on. If there are two errors though then this implementation is a little tedious, because it will only show one error at a time. So we need a way to collect the errors and give them to the user all at once.

Extending Maybe with State

When I initially came to this point, the simplest thing I could come up with was to pass a list to the validation functions. If the function had an error, put the error in the list, and return Nothing in a tuple with the error list. Otherwise return the result wrapped in Just in a tuple with the unaltered error list. Essentially the validation functions had the type signature of a -> ([String], Maybe a) One of the things that people will tell you is that if you have arguments constantly being threaded into functions then it is probably time to bring in some kind of state handling. So with this next iteration I used the type State [String] (Maybe a) as my return value for validation.


import Control.Applicative
import Control.Monad.State


type Age      = Int
type Username = String
data Form     = Form Age Username deriving (Show)

type FormState a = State [String] (Maybe a)


pushErr :: String -> FormState a
pushErr s = modify (s:) >> return Nothing


validateAge :: Age -> FormState Age
validateAge age
    | age > 0   = return . Just $ age
    | otherwise = pushErr "Age needs to be positive"


validateUsername :: Username -> FormState Username
validateUsername username
    | username `elem` usernames = pushErr "Username is already taken"
    | otherwise                 = return . Just $ username
  where
    --for simplicity, lets just use a list as our database of usernames
    usernames = ["foo", "bar", "baz"]


validateForm :: Age -> Username -> FormState Form
validateForm age username = do
    age'      <- validateAge age
    username' <- validateUsername username

    return (Form <$> age' <*> username')

So now we have state being threaded into the validation functions. If either of the validations have errors, they are pushed into the error stack and then return Nothing. In the form validation, we run both of the stateful validation computations, and then do the same thing with their result as we did in our Maybe data type iteration. So now when we do the form validation we see both of the errors at once if they both happen to be invalid.

Conclusion

So here is the final result of my work so far. Although this covers quite a bit of my use case, I can see a couple areas that could be made nicer still. Right now the code doesn’t do much for us when it comes to checking multiple invariants on the same piece of data. It’d be nice if we could hide that plumbing as well, but that will be for a later post.

comments powered by Disqus