Cool Monad Combinators

Haskell's if-statements work a bit differently from most other languages. In a language like C++ you can have an if statement that just has a single branch like this:

int sumOfList(const std::vector<int>& inputs, bool onlyHalf) {
  size_t listSize = inputs.size();
  if (onlyHalf) {
    // Only operate on half the list
    listSize = listSize / 2;
  }
  ...
}

But a statement like that doesn't strictly fit into Haskell's paradigms of limiting side effects and assigning types to all expressions. An if statement has to be an expression like everything else, and that expression must have a type. So the way Haskell does this is that an if statement must have two branches (if and else) and each branch must be an expression with the same result type.

But what about a situation where you just want to do conditional logging? Here's another quick example:

int sumOfList(const std::vector<int>& inputs, bool isVerbose) {
  if (isVerbose) {
    std::cout << "Taking sum of list..." << std::endl;
  }
  ...
}

In Haskell, we would need this function to be in the IO monad, since it's printing to the console. But how would we represent the "conditional logging portion? We'd have to make an "if statement" where each branch has the same type. A putStrLn expression has the type IO (), so we would need an empty expression of type IO () as the other branch. So in this case return () works.

sumOfList :: [Int] -> Bool -> m Int
sumOfList inputs isVerbose = do
  if isVerbose
    then putStrLn "Taking sum of list..."
    else return ()
  return $ foldl (+) 0 inputs

But it could be annoying to have this pattern of return () on a lot of different branches. But there are a couple useful combinators to help us: when and unless.

when :: Bool -> m () -> m ()

unless :: Bool -> m () -> m ()

These simply take a boolean value and a monadic action, and they only perform the action based on the boolean value. So we could rewrite our code from above:

sumOfList :: [Int] -> Bool -> m Int
sumOfList inputs isVerbose = do
  when isVerbose (putStrLn "Taking sum of list...")
  return $ foldl (+) 0 inputs

Now it looks a lot cleaner. With when, we perform the action whenever it's true. With unless, the action occurs only when the input is false.

We can observe though that these functions only work when the result value of the input is (), which is to say it has no result. Because when the alternative is that "nothing happens", we can't produce a result other than (). So these combinators are only sensible when there's some kind of side effect from the monadic action, like printing to the console, or modifying some stateful value.

Sometimes, you may have an action that produces a desired side effect but also returns a value.

printSize :: [Int] -> IO Int
printSize inputs = do
  putStrLn $ "Input has size: " ++ show (length inputs)
  return $ length inputs

If you want to use this with when or unless, you'll have to change the action so it instead returns (). This is the job of the void combinator. It just performs the action and then returns () at the end.

void :: m a -> m ()
void action = do
  _ <- action
  return ()

Now we could apply this with our different expressions above:

sumOfList :: [Int] -> Bool -> m Int
sumOfList inputs isVerbose = do
  when isVerbose (void $ printSize inputs)
  return $ foldl (+) 0 inputs

Hopefully you're able to use these combinators to make your Haskell a bit cleaner! If you're just getting started on your Haskell journey, you should download our Beginners Checklist! It'll provide you with some helpful tools to get going! Next week, we'll be back with some more monad tips!

Previous
Previous

An Alternative Approach

Next
Next

Does your Monad even Lift?