An Alternative Approach

Part of what monads do is that they encapsulate side effects. They typically include activity that is not a simple and pure calculation like 2 + 2. Because of this, there is a much higher chance that monadic actions will "fail" in some way. They won't be able to achieve the stated goal of the computation and will enter some kind of exceptional flow. There are a number of different classes that help us deal with these failures in reasonable ways. They'll also enable us to write code that can work with many different monads. These classes are Alternative, MonadFail, and MonadPlus.

The Alternative class works for all "applicative" types, so it actually applies more broadly than monads. But still, most of the common users of it are monads. As you might guess, this class allows us to say, "In case this action fails, do this instead." It has two primary functions: empty and the operator (<|>).

The empty function provides a "failure" condition of sorts. What happens when we can't execute the monadic (or applicative) action? For a monad like Maybe, the outcome of this is still something we could resolve with "pure" code. We just get the value Nothing.

instance Alternative Monad where
  empty = Nothing
  ...

However, IO provides a failure mechanism that will cause a runtime error if the operation doesn't succeed:

instance Alternative IO where
  empty = failIO "mzero"
  ...

Using failIO will throw an IOError that will crash our whole program unless it gets caught elsewhere. The word "mzero" is a default failure message that will make more sense in a second!

Now the (<|>) operator is, of course intended to look like the "or" operator (||). It takes two different actions as its inputs. It allows us to provide a different action to run if our first fails. So for Maybe, we can see if our first result is Nothing. And if so, we'll resolve the second Maybe value. Of course, this second value might also be Nothing! Using alternatives doesn't always guarantee success!

instance Alternative Monad where
  empty = Nothing
  m1 <|> m2 = case m1 of
    Nothing -> m2
    justValue -> justValue

We can see this in action:

>> let f x = if even x then Just (quot x 2) else Nothing
>> f 4
Just 2
>> f 3
Nothing
>> f 4 <|> f 8
Just 2
>> f 5 <|> f 8
Just 4
>> f 3 <|> f 5
Nothing

With IO, we actually need to catch the exception thrown by failIO and then perform the next action:

instance Alternative IO where
  empty = failIO "mzero"
  action1 <|> action2 = action1 `catchException` (\_ :: IOError -> action2)

Here's a quick look at these functions with an IO action:

>> readFile "does_not_exist.txt"
Error: openFile: does not exist (No such file or directory)
>> readFile "does_not_exist.txt" <|> readFile "does_exist.txt"
"Text in second file"

There are a couple other functions we can use with Alternative to provide a list of outcomes. The many function will take a single operation and run it repeatedly until the operation fails by returning empty:

many :: (Alternative f) => f a -> f [a]

In this case, we are guaranteed that the result of the function is never an error (empty)! If the first attempt at the operation fails, we'll get an empty list.

But if we want to enforce that it will succeed at least once, we can use some instead:

some :: (Alternative f) => f a -> f [a]

This cannot return an empty list. If the first result is empty, it will give the failure action.

It may seem a little odd that these functions take only a single action, rather than a list of actions. But these functions (and the Alternative class in general) lie at the heart of many parsing operations! You can learn more about that in our Parsing series.

There are a couple other classes that build on these Alternative ideas. The MonadFail class has one function: fail. This function takes a string as an argument and will perform an appropriate failure action, often involving a runtime error:

class MonadFail m where
  fail :: String -> m a

Then there's MonadPlus. This takes the essential activities of Alternative and raises them to be monad-specific. It has mzero, which mimics empty, and mplus, which works like (<|>).

instance (Alternative m, Monad m) => MonadPlus m where
  mzero :: m a
  mplus :: m a -> m a -> m a

Often, the underlying alternative functions are used as the default instances, and there is no change in behavior. I personally find it a little confusing that "plus" is an "or" operation instead of "and" operation. I would expect that "adding" two operations would perform the first and then the second in succession. But this isn't what happens! If the first succeeds, the second never occurs.

Hopefully these different classes help you to write cleaner monadic operations. To learn more about the basics and fundamentals of monads, you should read our series on Monads and Functional Structures and subscribe to our newsletter!

Previous
Previous

Making your own Monad

Next
Next

Cool Monad Combinators