Please Buy now at Leanpub
Written and illustrated by GetContented.
Published on 2017-06-25.
In this chapter we’ll look at all the ways we can make code that gives results that depend on values.
First up, let’s look at a program that takes a name, then prints a message, depending on what the name is.
message :: String -> String message name = if name == "Dave" then "I can't do that." else "Hello." main :: IO () main = putStrLn (message "Dave")
message is a function that takes a
String and returns a
String it takes as an argument is named
name in the body of the function. It’s pattern-matched into the
name variable, is another way to say this.
if...then...else expression is one way we can control what will happen in Haskell programs.
It’s very good for what’s called a two-way branch; that is, when there are two “paths” to take: the path for
True or the path for
False. (It’s either
"Dave", or it’s
The result of the
if expression is dependent on the variable named
So what about this
(==) operator that we saw in that program? Let’s look at its type:
(==) :: Eq a => a -> a -> Bool
Here we have a new typeclass constraint, this time called
Eq. This operator works out if its two arguments evaluate to the same value. If so, it returns the
Bool is the type which comprises just the values
False. These are used to express whether things are true or not in Haskell.
Eq provides the
(/=) operators. They mean “is equal to”, and “is not equal to” respectively.
Eq is short for equal or equality.
if expression has three sections, and it must always have three sections. The first section is an expression that must evaluate to a
Bool value. You can begin to see why
(==) is quite an important operator now, can’t you? When it evaluates to
True, the expression that follows “then” is returned, otherwise the expression after the “else” is returned.
if expression always results in a single value, both of its return expressions must have the same type.
So, what if we want to actually test for more than just two alternatives? (maybe “Dave”, “Sam” or other).
Well we can “chain” these
if expressions by putting another one into the first one:
message :: String -> String message name = if name == "Dave" then "I can't do that." else if name == "Sam" then "Play it again." else "Hello."
So now, it’ll check if the name is Dave: if so, it’ll respond with “I can’t do that” as before, otherwise if the name is Sam, it’ll respond with “Play it again”, and if it’s neither, it’ll be “Hello”. Phew! Look at that if expression! What a mouthful. And this will only get more annoying as we add more options.
Let's see the same program but using the
(/=) operator instead:
message :: String -> String message name = if name /= "Dave" then if name == "Sam" then "Play it again." else "Hello." else "I can't do that."
No surprises here, we just have to flip the branches around as we’ve done.
Having this many branches in our
if expressions is not very easy to read. Let’s look at a better way to do the same thing. Before we do this, though, it should be mentioned if you're writing these programs in, then realise that the spacing matters in Haskell! So, we must indent our lines properly. There is actually another way we can write haskell which uses lots of punctuation instead of spacing, but spacing looks nicer, so we will use that.
Here we’re using a case expression. You can see how it works pretty easily when comparing it to the nested if expressions from our previous example.
message :: String -> String message name = case name of "Dave" -> "I can't do that." "Sam" -> "Play it again." _ -> "Hello." main :: IO () main = putStrLn (message "Dave")
To evaluate a
case expression, the expression between “
case” and “
of” is first evaluated, then Haskell will run through all the patterns we have given it on the left of the
-> symbols, and try to pattern-match the value with them. If it finds a match, it returns the corresponding expression to the right of the
Case expressions are incredibly powerful because of the pattern matching we can do. Here we’ve just shown you an extremely basic example where the single name expression
name is matched to simple value
What about the underscore (
_) pattern? This pattern matches everything in Haskell, and it's included to make sure any time our function is called in the future with something we didn't anticipate, it will still work. Notice that the order matters. Dave will be matched before Sam. In this example it's not so important. Take a look at the following example, though:
message :: String -> String message name = case name of _ -> "Hello." "Dave" -> "I can't do that." "Sam" -> "Play it again." main :: IO () main = putStrLn (message "Dave")
The order matters! In this case, even if
"Dave", the code will never get that far, because the underscore matches on everything, and it’s first in the list!
In this case, our program will compile just fine, but it’s not what we want. This is called a logical error, because while it’s syntactically correct, it doesn’t have the correct logic. If we compile this with a Haskell compiler such as GHC, it will issue us a pattern-match overlap warning, letting us know that we’ve got multiple paths of logic flow for the same inputs.
Do note, also, that all of the types of the result expressions have to be the same. The same rule applies from the
if expressions, above. You can’t have a different result type in any of the expressions on the right. The whole
case expression is a single expression, so it must result in a value of a single type.
Let’s look at yet another way to do the same thing, this time using what’s called a guard pattern:
message :: String -> String message name | name == "Dave" = "I can't do that." | name == "Sam" = "Play it again." | otherwise = "Hello."
So we immediately notice that the function definition’s
= symbol is gone from the right hand side. It no longer says
message name = ... but rather there are multiple “definitions” each with a pipe (
|) symbol in front of them.
The way this works is that the expressions on the left are tested for equality to
True, in order. When a
True value is found, it returns the expression to the right of the = sign that corresponds to that expression. This is not pattern matching like in the case expression, but rather test for truth, so it’s subtlely different. This is quite good for when you to test for several things.
Also notice that where a
if expression can actually be inserted anywhere you like, this one is only usable in named function definitions. You can't use this form within a lambda, for example.
You can also see that we’re using something called
otherwise here as our default expression value. The
otherwise identifier is defined very simple, as
True, so we could have written this:
message :: String -> String message name | name == "Dave" = "I can't do that." | name == "Sam" = "Play it again." | True = "Hello."
Which of course, because it’s testing for
True, will always match. The only difference is,
otherwise actually means something to human programmers, so that’s why we use it.
Wherever possible, we should endeavour to make our programs as clear as possible for our future selves, and others who may want to read our code. Sometimes it’s quite literally the difference between our code being used or not.
There’s still one more way we can write this, so let’s see that now. This is just a simple regular function definition, but with many definitions, and using pattern matching on values in the argument list:
message :: String -> String message "Dave" = "I can't do that." message "Sam" = "Play it again." message _ = "Hello." main :: IO () main = putStrLn (message "Dave")
Here we’re using pattern matching again, but directly on the argument list. Notice that the
name variable is gone entirely, and our old friend the underscore is back. That’s because we’re directly pattern matching on what the
name value would be.
In the simple examples we’ve shown, none of the techniques stand out as obviously better or worse, except the if expression when we want to match on more than one thing.
The case expression is probably the best fit, because there’s less repetition. Each have benefits and drawbacks, and we’ll see more of each of them as we proceed. This section is mostly to get you familiar with them.
As what we need changes, the different conditionals will make more or less sense. What if we needed to detect if the name started with a “D”? In that case, the if expression or guard pattern would make more sense to use than the others. The case expression, or other direct pattern matching style examples wouldn’t make sense there.
Let’s finish by adding another simple clause to our case expression example:
message :: String -> String message name = case name of "Dave" -> "I can't do that." "Sam" -> "Play it again." "Creep" -> "Feeling lucky?" _ -> "Hello." main :: IO () main = putStrLn (message "Dave")
Two parts to your homework. First is to try all the examples yourself. Experiment with changing
"Dave" to other names, then do an internet search for examples of each type of conditional technique and recognise the pieces.
Don’t get too worried that they won’t look as simple as our examples here. Just use our examples as a kind of guide, and try to pick out the pieces you do recognise, and don’t get confused if the examples you find look crazy. Ignore the crazy for now and look for the parts you do recognise.
The second part of your homework is to help reinforce your understanding of currying. Maybe you forgot what that was. It’s when we use two or more functions wrapped around themselves to make a way for a function to take multiple arguments.
Let’s look at a function that might look a little bit strange at first:
addThem :: Int -> Int -> Int -> Int -> Int addThem a b c d = a + b + c + d
You can probably work out that this function adds its four arguments together. Of course, what we really mean is that it has the effect of having four arguments and adding them together, but we understand what is really happening when we define a function like this. What is really happening is that Haskell builds a “multi-level” function that looks like the following (notice the indentation and how each function argument lines up with an
Int in the type signature):
addThem :: Int -> (Int -> (Int -> (Int -> Int))) addThem = \a -> (\b -> (\c -> (\d -> a + b + c + d)))
And, because the way function application works in Haskell, we can apply values to it in this way
addThem 1 2 3 4, then it will return
So, we can see from this that
addThem is defined as a function that takes a variable called
a and returns a function. That function uses
a deep inside its bowels; that second function is a function that takes a variable called
b, and so on, until you get to the inner function body of the function that takes
d as its argument. You can see how these variables match up to the type signature above it, which we’ve put parentheses around to group them, showing the way Haskell builds the functions up more clearly.
Well, to unpack this a little bit more, let’s make five different definitions to show each step of this process, and provide you with type annotations of each of those definitions. First up, we’ll create a definition that where we apply
1 to the
a variable, and so we can remove its outer function wrapper, the one with the
a variable in it:
-- here we have applied the addThem function to 1 addThemOne :: Int -> Int -> Int -> Int addThemOne = addThem 1 -- which looks like this: -- addThem 1 == \b -> (\c -> (\d -> 1 + b + c + d)) -- this is the same thing as applying the value 1 -- to that function / lambda: -- (\a -> (\b -> (\c -> (\d -> a + b + c + d)))) 1
We can see from that quite clearly that
1 has been substituted in for
a within the function. We don’t need the outer part of the function syntax
\a -> any more because we’ve applied that function to the value
1, which makes it disappear.
Next we’ll satisfy the
b variable with the value
2, and we can therefore also similarly remove that function wrapper, because it has been applied:
-- here we have injected 2 into the addThemOne function addThemOneToTwo :: Int -> Int -> Int addThemOneToTwo = addThemOne 2 -- which is the same thing as -- addThem 1 2 -- and would look like this: -- addThemOne 2 == (\c -> (\d -> 1 + 2 + c + d)) -- which is the same thing as this -- function application, ie applying 2 to it: -- (\b -> (\c -> (\d -> 1 + b + c + d))) 2
Next we will place
3 into the
c variable. Are you noticing the type signatures? They’re getting smaller by one with each application of our function. This should make sense because each time we’re building a new function by applying a value to a previous function which produces a function of one less argument. Let’s continue:
-- here we have applied the addThemOneToTwo -- function to the value 3 addThemOneToThree :: Int -> Int addThemOneToThree = addThemOneToTwo 3 -- which is the same thing as -- addThem 1 2 3 -- or addThemOne 2 3 -- and would look like this: -- addThemOneToTwo 3 == (\d -> 1 + 2 + 3 + d) -- again, this is the same as this -- function application: -- (\c -> (\d -> 1 + 2 + c + d)) 3
Finally, we put the value
-- here we take 4 and put it into -- the addThemOneToThree function addThemOneToFour :: Int addThemOneToFour = addThemOneToThree 4 -- which is the same thing as -- addThem 1 2 3 4 or -- addThemOne 2 3 4 or -- addThemOneToTwo 3 4 or -- and would look like this: -- addThemOneToThree 4 == 1 + 2 + 3 + 4 -- which equals 10 -- again, this is the same as this -- function application: -- (\d -> 1 + 2 + 3 + d) 4
We hope this is clear, if it’s not at all, we’d really appreciate your feedback. You can give it by following the feedback link on the main page.
If you’ve enjoyed reading this, please consider purchasing a copy at Leanpub today
Please follow us, and check out our videos Follow @HappyLearnTutes
Also, Volume 2 is now in beta and being written! Show your support and register your interest at its Leanpub site.