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:
(Refined _)
matches on theRefined
data constructor but discards the value it contains. We do this because we don't need the value for the message we return.(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:
- In Haskell, you don't have to wrap the
if
condition in parentheses like you do in many other languages. - According to its type signature, this function returns a
Bool
value. We see from the if-expression that indeed, we implicitly returnTrue
in one case orFalse
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.