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 Stringnewtype Password = Password Stringnewtype Age = Age IntvalidateEmail :: String -> Maybe EmailvalidateEmail input = if '@' member input then Just (Email input) else NothingvalidatePassword :: String -> Maybe PasswordvalidatePassword input = if length input > 12 then Just (Password input) else NothingvalidateAge :: String -> Maybe AgevalidateAge 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 AgeprocessInputs :: (String, String, String) -> Maybe UserprocessInputs (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 EmailvalidateEmail input = if '@' member input then Right (Email input) else Left (BadEmail input)validatePassword :: String -> Either ValidationError PasswordvalidatePassword input = if length input > 12 then Right (Password input) else Left (BadPassword input)validateAge :: String -> Either ValidationError AgevalidateAge 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 UserprocessInputs (i1, i2, i3) = do email <- validateEmail i1 password <- validatePassword i2 age <- validateAge i3 return $ User email password agecreateUser :: 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 StringparseIntOrString 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:
- If the first letter of the string is capitalized, return the third letter. Otherwise, drop the first letter from the string.
- 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 Stringrule1 input = if isUpper (head input) then Left (input !! 2) else Right (tail input)rule2 :: String -> Either Char Stringrule2 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 StringsolveRules 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!