A Month of Haskell, Day 1 - Control.Conditional

Posted on May 1, 2017 by Chris Lumens in month-of-haskell.

In the spirit of 24 Days of GHC Extensions and 24 Days of Hackage I thought i would start my own series of posts on useful things about the Haskell programming language. While I am a big fan of the language, I have some complaints about the documentation. It tends to either be non-existent, written for someone who is alredy an expert with the language, or assumes familiarity with advanced mathematics.

The internet is littered with dense web pages about bifunctors and covariants and arrows and lifting. I am hoping to explain things that have been useful to me in much more plain language, aimed at someone who is more interested in the programming than the theory. While I am going to assume some familiarity with Haskell, I’m not going to assume you are an expert.

The Control.Conditional module is provided by the cond package and contains a lot of functions that are helpful for only doing things sometimes. None of these functions are completely necessary - you can write the same code in different ways. But they do allow for more concise and readable code.

Before getting into the contents of Control.Conditional, it’s worth talking about unless and when from Control.Monad.

Type signatures:

unless :: bool -> f () -> f ()
when :: bool -> f () -> f ()

These two functions take a boolean and a function, and possibly run the function. They operate on monads. There’s a million tutorials about monads, and I’ll go into them in greater depth in a future post. Because this isn’t a blog about teaching yourself Haskell, I am assuming you have some familiarity with monads. If not, know they are a type class:

Type classes:

class Applicative m => Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    (>>) :: m a -> m b -> m b
    return :: a -> m a

when lets you replace code that looks like this:

import System.Environment(getArgs)

args <- getArgs
if (length args /= 1) then putStrLn "One argument is required"
else return ()

With code that looks like this:

import System.Environment(getArgs)

args <- getArgs
when (length args /= 1) $
    putStrLn "One argument is required"

This example doesn’t result in much shorter code, but it’s nicer to read and if you’re used to imperative programming, it might look more natural to you. You can imagine how it could make other things shorter, though. unless does the exact same thing except only when the boolean is false.

I started with that because it’s a good way to get into Control.Conditional. Among other things, it provides whenM and unlessM. These do the exact same things as when and unless, just with monads involved. So instead of a boolean it takes an m bool. bool is something the module provides and for most purposes it’s safe to assume it’s the same as the built-in boolean type.

Type signatures:

unlessM :: m bool -> m () -> m ()
whenM :: m bool -> m () -> m ()

This allows you to take this code:

import System.Directory(doesFileExist, removeFile)

exists <- doesFileExist "/tmp/lockfile"
if exists then removeFile "/tmp/lockfile"
else return ()

And rewrite it more simply like this:

import Control.Conditional(whenM)
import System.Directory(doesFileExist, removeFile)

whenM (doesFileExist "/tmp/lockfile") $
    removeFile "/tmp/lockfile"

There’s also an entire set of functions that act like all the normal boolean operators, but involving monads. There’s ifM which is similar to the if keyword, notM and xorM that act like not and xor, and the <||> and <&&> functions that act like || (or) and && (and). These can be combined in all the usual ways.

Type signatures:

ifM :: m bool -> m a -> m a -> m a
notM :: m bool -> m bool
xorM :: m bool -> m bool -> m bool
<||> :: m bool -> m bool -> m bool
<&&> :: m bool -> m bool -> m bool

All the arguments to all these functions involve monads, and all the return values involve monads too. What happens if you want one of the arguments to be something that’s not a monad? You have to return the value to put it in a monad. Then you can use it. Here’s an example:

import Control.Conditional(ifM)
import System.Directory(doesFileExist)

discover :: IO (Either String FilePath)
discover = ifM (doesFileExist "/tmp/lockfile")
               (return $ Left "File already exists")
               (return $ Right "/tmp/lockfile")

The other functions operate like you would expect:

import Control.Conditional(ifM, notM)
import System.Directory(doesFileExist)

discover :: IO (Either String FilePath)
discover = ifM (notM $ doesFileExist "/tmp/lockfile")
               (return $ Left "File not found")
               (return $ Right "/tmp/lockfile")
import Control.Conditional(ifM, (<&&>))
import System.Directory(doesFileExist, pathIsSymbolicLink)

fileIsLink :: FilePath -> IO String
fileIsLink fp = ifM (doesFileExist fp <&&> pathIsSymbolicLink fp)
                    (return "File is a symbolic link")
                    (return "File is not a symbolic link")

And then there’s a set of functions that mimic Haskell’s case statement, or perhaps the cond function from Lisp, if that’s more familiar to you. In fact one of them is named cond and it is useful when you need to take action based on many different conditionals.

Type signatures:

cond :: [(bool, a)] -> a

ghc also has the MultiWayIf language extension to fill this role.

import Control.Conditional(cond)

strcmp :: String -> String -> Int
strcmp a b = cond [(a < b,  -1),
                   (a == b,  0),
                   (a > b,   1)]

You could also use otherwise to handle the default case:

import Control.Conditional(cond)

strcmp :: String -> String -> Int
strcmp a b = cond [(a < b,     -1),
                   (a > b,      1),
                   (otherwise,  0)]

condM works very similarly, and also makes use of otherwiseM for catching any cases that weren’t included.

Type signatures:

condM :: [(m bool, m a)] -> m a

Again, all the arguments must be monadic in nature. That means using return if necessary:

import Control.Conditional(condM, otherwiseM)

data OperationMode = NormalMode | SatelliteMode | SOTAMode

currentOperationMode :: IO OperationMode
currentOperationMode =
    condM [(satelliteModeActive, return SatelliteMode),
           (sotaModeActive,      return SOTAMode),
           (otherwiseM,          return NormalMode)]

This is all pretty simple stuff, but it can make for much more readable code. Eliminating intermediate steps and variables like this helps to get right to what is actually happening and removes the obfuscation. That is one of Haskell’s greatest strengths.

The Control.Conditional module provides lots of other operators like ?? and |> and selectM, but I’ve never had to use them. It’s possible I’ve just never written anything that would benefit from them, or that I don’t really understand when they are useful. One caution is that using too many operator-like functions and too much clever code can make your code harder to understand both by other people and by yourself later.