Using Either as a Monad

Now that February is over and we're into March, it's time for "Monads Month"! Over the course of the next month I'll be giving some helpful tips on different ways to use monads.

Today I'll start with a simple observation: the Either type is a monad! For a long time, I used Either as if it were just a normal type with no special rules. But its monadic behavior allows us to chain together several computations with it with ease!

Let's start from the beginning. What does Either look like? Well, it's a very basic type that can essentially hold one of two types at runtime. It takes two type parameters and has two corresponding constructors. If it is "Left", then it will hold a value of the first type. If it is "Right" then it will hold a value of the second type.

data Either a b = Left a | Right b

A common semantic understanding of Either is that it is an extension of Maybe. The Maybe type allows our computations to either succeed and produce a Just result or fail and produce Nothing. We can follow this pattern in Either except that failures now produce some kind of object (the first type parameter) that allows us to distinguish different kinds of failures from each other.

Here's a basic example I like to give. Suppose we are validating a user registration, where they give us their email, their password, and their age. We'll provide simple functions for validating each of these input strings and converting them into newtype values:

newtype Email = Email String
newtype Password = Password String
newtype Age = Age Int

validateEmail :: String -> Maybe Email
validateEmail input = if '@' member input
  then Just (Email input)
  else Nothing

validatePassword :: String -> Maybe Password
validatePassword input = if length input > 12
  then Just (Password input)
  else Nothing

validateAge :: String -> Maybe Age
validateAge input = case (readMaybe input :: Maybe Int) of
  Nothing -> Nothing
  Just a -> Just (Age a)

We can then chain these operations together using the monadic behavior of Maybe, which short-circuits the computation if Nothing is encountered.

data User = User Email Password Age

processInputs :: (String, String, String) -> Maybe User
processInputs (i1, i2, i3) = do
  email <- validateEmail i1
  password <- validatePassword i2
  age <- validateAge i3
  return $ User email password age

However, our final function won't have much to say about what the error was. It can only tell us that an error occurred. It can't tell us which input was problematic:

createUser :: IO (Maybe User)
createUser = do
  i1 <- getLine
  i2 <- getLine
  i3 <- getLine
  result <- processInputs (i1, i2, i3)
  case result of
    Nothing -> print "Couldn't create user from those inputs!" >> return Nothing
    Just u -> return (Just u)

We can extend this example to use Either instead of Maybe. We can make a ValidationError type that will help explain which kind of error a user encountered. Then we'll update each function to return Left ValidationError instead of Nothing in the failure cases.

data ValidationError =
  BadEmail String |
  BadPassword String |
  BadAge String
  deriving (Show)

validateEmail :: String -> Either ValidationError Email
validateEmail input = if '@' member input
  then Right (Email input)
  else Left (BadEmail input)

validatePassword :: String -> Either ValidationError Password
validatePassword input = if length input > 12
  then Right (Password input)
  else Left (BadPassword input)

validateAge :: String -> Either ValidationError Age
validateAge input = case (readMaybe input :: Maybe Int) of
  Nothing -> Left (BadAge input)
  Just a -> Right (Age a)

Because Either is a monad that follows the same short-circuiting pattern as Maybe, we can also chain these operations together. Only now, the result we give will have more information.

processInputs :: (String, String, String) -> Either ValidationError User
processInputs (i1, i2, i3) = do
  email <- validateEmail i1
  password <- validatePassword i2
  age <- validateAge i3
  return $ User email password age

createUser :: IO (Either ValidationError User)
createUser = do
  i1 <- getLine
  i2 <- getLine
  i3 <- getLine
  result <- processInputs (i1, i2, i3)
  case result of
    Left e -> print ("Validation Error: " ++ show e) >> return e
    Right u -> return (Right u)

Whereas Maybe gives us the monadic context of "this computation may fail", Either can extend this context to say, "If this fails, the program will give you an error why."

Of course, it's not mandatory to view Either in this way. You can simply use it as a value that could hold two arbitrary types with no error relationship:

parseIntOrString :: String -> Either Int String
parseIntOrString input = case (readMaybe input :: Maybe Int) of
  Nothing -> Right input
  Just i -> Left i

This is completely valid, you just might not find much use for the Monad instance.

But you might find the monadic behavior helpful by making the Left value represent a successful case. Suppose you're writing a function to deal with a multi-layered logic puzzle. For a simple example:

  1. If the first letter of the string is capitalized, return the third letter. Otherwise, drop the first letter from the string.
  2. If the third letter in the remainder is an 'a', return the final character. Otherwise, drop the last letter from the string. 3 (and so on with similar rules)

We can encode each rule as an Either function:

rule1 :: String -> Either Char String
rule1 input = if isUpper (head input)
  then Left (input !! 2)
  else Right (tail input)

rule2 :: String -> Either Char String
rule2 input = if (input !! 2 == 'a')
  then Left (last input)
  else Right (init input)

rule3 :: String -> Either Char String
...

To solve this problem, we can use the Either monad!

solveRules :: String -> Either Char String
solveRules input = do
  result1 <- rule1 input
  result2 <- rule2 result1
  ...

If you want to learn more about monads, you should check out our blog series! For a systematic, in depth introduction to the concept, you can also take our Making Sense of Monads course!

Previous
Previous

Running with Monads

Next
Next

Treating Strings like Lists