Branching in Haskell

Branching in Haskell

Overview

In Haskell, there are multiple ways to branch your code's control flow based on some condition. Let's cover those ways in depth.

Pattern matching

You will see pattern matching everywhere in Haskell code. All of the examples in this section show pattern matching in the context of function definitions, but, per the spec:

Patterns appear in lambda abstractions, function definitions, pattern bindings, list comprehensions, do expressions, and case expressions.

Thus, as you're reading the below examples, keep in mind that all of the patterns shown could be dropped into any of the other contexts listed above. I have chosen to show pattern matching in the context of function definitions simply because I think it's the best for instructional purposes.

Example 1: Maybe

getValue :: Maybe Int -> Int
getValue (Just x) = x
getValue Nothing = 0

In this example, on line 1, we define the type signature of a function called getValue that accepts a Maybe Int (basically a nullable int) and returns an int. Let's see the definition of the Maybe data type:

data Maybe a = Just a | Nothing

This means that we can either have a value of any type a (in our case, Int), or nothing at all. Now the two function definitions on lines 2 and 3 in the example above make sense:

If there IS a value, just return it:

getValue (Just x) = x

Otherwise, return a default value of zero:

getValue Nothing = 0

Let's look at another example.

Example 2: Either

Here we are matching on a value of type Either String Int:

printVal :: Either String Int -> IO ()
printVal (Left error) = print error
printVal (Right val) = print $ show val

The Either data type is commonly used in Haskell to represent operations that can have a success state (Right) and an error state (Left). Here it is:

data Either a b = Left a | Right b

In our code above, we pattern match on both data constructors available: Left and Right, and we do something slightly different in either case.

Example 3: Custom data types

In general, any custom data type you write can be pattern matched in the way shown above. For example:

data Fancy person clothes = Dapper person clothes | Classy person | Refined person

In this fanciful type definition, we have defined three data constructors: Dapper, Classy, and Refined. But there's a twist! Only Dapper accepts a second parameter (clothes). We can match on this data type like so:

saySomethingFancy :: Fancy String String -> String
saySomethingFancy (Classy name) = "You are brimming with class, " ++ name
saySomethingFancy (Refined _) = "Bold, yet refined."
saySomethingFancy (Dapper name clothes) = "Say, " ++ name ++ ", you're looking dapper in those " ++ clothes

Notice that we did a couple special things here:

  1. (Refined _) matches on the Refined data constructor but discards the value it contains. We do this because we don't need the value for the message we return.
  2. (Dapper name clothes) is an example of matching a data constructor with multiple arguments. It works just the same as matching on a single argument.

Example 4: Lists and as-patterns

Now let's look at how you can pattern match on a list in Haskell. (Yes, I wanted to pack as many keywords as possible into that sentence.) Here's an example:

first :: [a] -> Maybe a
first (x:_) = Just x
first [] = Nothing

Here, we've defined a function that will return Just the first value of a list, or if the list is empty, Nothing. (Note that this is safer than the built-in head function)

Notice how we pattern match on the nonempty list: (x:_). This means that we want exactly the first element; we discard the rest with _ (which is called a wildcard). If we wanted the rest of the elements, we could write (x:xs) or (firstEl:restOfEls) or whatever we wanted to name the variables. In this scenario, the xs or restOfEls variable would be of type [a] because it's a list containing the rest of the elements.

We pattern match on the empty list with [], which is fairly self-explanatory.

We can get fancy and pattern match on lists of any length with repeated applications of the :. For example: (x:y:z:rest) would give us the first three elements as named variables x,y,z, then return the rest as rest.

One more example:

firstOfAll :: [a] -> (a, [a])
firstOfAll all@(x:_) = (x, all)

Here, we use an as-pattern (all@), which allows us to save the whole value we're matching against as a variable for later use. This can be very helpful in cases like the above where you need something from inside the data structure and also the whole structure itself.

(Note that the above example is naughty because it is an incomplete pattern.)

Read more about pattern matching in the language spec.

If-else expressions

This is the most straightforward conditional expression in Haskell, and is mirrored in basically every programming language known to humans. Here it is:

if <expression> then <value1> else <value2>

The important thing to notice here is the use of the word "expression." If-expressions always return a value (either <value1> or <value2>).

Example

isCat :: String -> Bool
isCat s = if s == "cat" then True else False

Notes:

  1. In Haskell, you don't have to wrap the if condition in parentheses like you do in many other languages.
  2. According to its type signature, this function returns a Bool value. We see from the if-expression that indeed, we implicitly return True in one case or False in all other cases.

Guards

Guards are a nice way of writing out more complex boolean conditions in a form that is a little less clunky than if-expressions. (However, they can't be used in all the same contexts as if-expressions.)

Example

shouldInviteToBankHeist :: String -> Int -> Bool
shouldInviteToBankHeist name trustworthiness
  | name == "Gal Gadot" = True
  | name == "Ryan Reynolds" = False
  | trustworthiness > 5 = True
  | name == "Unreliable Jim" && trustworthiness > 10 = True
  | otherwise = False

Guards are denoted by the pipe operator | and are useful for evaluating somewhat complex boolean expressions containing && and ||.

Case expressions

Case expressions are similar to switch statements in C-like languages, but more powerful.

Example 1

You can use pattern matching in case expressions. In fact, pattern matching in every context except for case expressions translates down to case expressions. All the other contexts, like matching in function definitions, lambdas, list comprehensions, etc, are syntactic sugar over case expressions (reference).

sayHello :: String -> Maybe String
sayHello name = case name of
  "" -> Nothing
  "Danny boy" -> Just "The pipes, the pipes are calling"
  name -> Just $ "Hello there, " ++ name

This should look familiar by now. We match on a couple specific cases, then end with a catch-all case of name -> Just .... Slightly different syntax from the pattern matching examples we gave above, but same idea. You can use wildcards, as-patterns, list patterns, and any other kind of legal pattern you can dream up.

Example 2: Cases with guards

We can use guards in case expressions to do even more fancy footwork. For example:

sayHello :: String -> Maybe String
sayHello name = case name of
  "" | length name < 3 -> Nothing
  "Danny boy" -> Just "The pipes, the pipes are calling"
  name -> Just $ "Hello there, " ++ name

See how we put the | length name < 3 check in there? You can add as many guards like that as you want into any of the tests in your case expression. In this way, we are afforded a great deal of flexibility in handling complex cases that arise in our code (reference).

Conclusion

There you have it. Now you know how to make like a tree and branch. Thanks for reading.