Modules and Function Syntax
Welcome back to the Monday Morning Haskell Liftoff series! This is part 2 of the series. In case you missed it, you should check out part 1, where we downloaded and installed all our tools. We also went through the fundamental concepts of expressions, types, and functions.
At this point you might be thinking to yourself, "learning about types with the interpreter is fine. But I want to actually write code!" What does Haskell's syntax really look like? Luckily, that's exactly the focus of this article!
We'll start writing our own modules and functions. We'll see how to read our code into the interpreter and how to run it as an executable. We'll also learn more advanced function syntax to describe more complicated ideas. In part 3 of this series, we'll check out how we create our own data types!
If you want to follow along with the code examples in this part, you can head to the companion Github repository! We'll provide links to the code in a couple different spots throughout the article!
You should also check out our Beginner's Checklist! It will help your review this series and point you towards some more resources that can aid you in your quest to learn Haskell!
Writing Source Files
Now that we're familiar with the basic concepts of Haskell, we should start writing our own code. For this first part of the article, you can refer to our reference file here on Github. Or you can write it all on your own, either way! Let's start by opening up a file called MyFirstModule.hs
, and declare it as a Haskell module by using the "module" keyword at the top.
module MyFirstModule where
The where
statement follows the name of the module and represents the "starting point" of our code. Let's write a very basic expression that our module will export. We assign the expression to a name using the equals sign. Unlike the interpreter, we do not need to use the keyword "let" here.
myFirstExpression = "Hello World!"
When defining expressions within a module, it is common practice to put a type signature on all your top level expressions and functions. This is vital documentation for anyone trying to read your code. It also helps the compiler infer types within your code's sub-expressions. So let's go ahead and label this expression as a string using the ::
operator.
myFirstExpression :: String
myFirstExpression = "Hello World!"
Let's also define our first function. This will take a string as an input, and append the input to the string "Hello". Notice how we define the function type using the arrow from the input type to the output type.
myFirstFunction :: String -> String
myFirstFunction input = "Hello " ++ input
Now that we have this code, we can load our module into GHCi. To do this, run GHCi from the same directory containing the module. You can use the :load
command to load all the expression definitions in so that you can access them. Let's see this in action:
>> :load MyFirstModule
(loaded)
>> myFirstExpression
"Hello World!"
>> myFirstFunction "Friend"
"Hello Friend"
If we modify our original source code, we can then go back and reload the module in GHCi using the :r
command ("reload"). Let's change the function to:
myFirstFunction :: String -> String
myFirstFunction input = "Hello " ++ input ++ "!"
And now we reload it and run it again!
>> :r
(reloaded)
>> myFirstFunction "Friend"
"Hello Friend!"
Input and Output
So eventually we want to be able to run our code without needing to use the interpreter. To do this, we'll turn our module into an executable. We do this by adding a function named main
with a special type signature:
main :: IO ()
This type signature might seem a little odd, since we haven't talked about IO
or ()
yet. All you need to understand for now is that this type signature allows our main function to interact with the terminal. We can, for instance, make it run some print statements. We'll use a special syntax called "do-syntax". We use the word do
, and then list a printing action on each line below it.
main :: IO ()
main = do
putStrLn "Running Main!"
putStrLn "How Exciting!"
Now that we have this main function, we no longer need to use the interpreter. We can use the terminal command "runghc".
> runghc ./MyFirstModule
Running Main!
How Exciting!
Of course, you'll also want to be able to read input from your user, and call different functions on it. You get input using the getLine
function. You can access the input using the special <-
operator. Then within do-syntax, you can now use "let" like you could in the interpreter to assign an expression to a name. In this case, we'll call our previous expression.
main :: IO ()
main = do
putStrLn "Enter Your Name!"
name <- getLine
let message = myFirstFunction name
putStrLn message
Now let's try running it.
> runghc ./MyFirstModule.hs
Enter Your Name!
Alex
Hello Alex!
And just like that, we've written our first small Haskell program!
If and Else Syntax
Now we're going to shift gears a little bit, and look into how we can make our functions more interesting by using Haskell's syntax constructs. There are two options for following along here. You can refer to the complete file which has all the final code we'll write for this part. Or you can use the do-it-yourself file where you get to fill in the definitions yourself as you go along!
The first syntax concept we'll explore is if-statements. Let's suppose we want to ask the user to enter a number. Then we'll do different things based on how large the number is.
If statements are a bit different in Haskell than they are in other languages. For instance, the following statement makes perfect sense in Java:
if (a <= 2) {
a = 4;
}
Such an if-statement cannot exist in Haskell! All if-statements must have an else branch! To understand why, we have to think back to the fundamentals from the last article. Remember, everything in Haskell is an expression, and every expression has a type. Since we could assign the expression to a name, what would it mean for that name if the statement turned out to be false? Let's look at an example of a correct if statement:
myIfStatement a = if a <= 2
then a + 2
else a - 2
This is a complete expression. On the first line, we write an expression of type Bool
, which evaluates to True
, or False
. Then on the second line, we write an expression that will be the result if the boolean is true. The third line is a different expression that will be used if the result is false.
Remember every expression has a type. So what is the type of this if-expression? Suppose our input is an Int
. In this case, both the branches (a+2
and a - 2
) are also ints, so the type of our expression must be an Int
itself.
myIfStatement :: Int -> Int
myIfStatement a = if a <= 2
then a + 2
else a - 2
What happens if we try to make it so that the lines have different types?
myIfStatement :: Int -> ???
myIfStatement a = if a <= 2
then a + 2
else "Hello"
This will result in an error no matter what type we try to substitute as the result! This is an important lesson for if statements. You have two branches, and they each have to result in the same type. The resulting type is the type of the whole statement.
As a side note, our examples will tend to use a particular style of indentation. However, you can put the if-statement on one line if you like as well:
myIfStatement :: Int -> Int
myIfStatement a = if a <= 2 then a + 2 else a - 2
Note that Haskell does not have an "elif" statement like Python. You can still achieve this effect though! You can use an if-statement as the entire expression for an else-branch.
myIfStatement :: Int -> Int
myIfStatement a = if a <= 2
then a + 2
else if a <= 6
then a
else a - 2
Guards
In situations where you may have many different cases though, it can be more readable to use guards in your code. Guards allow you to check on any number of different conditions. We can rewrite the code above using guards like so:
myGuardStatement :: Int -> Int
myGuardStatement a
| a <= 2 = a + 2
| a <= 6 = a
| otherwise = a - 2
There are a couple tricky parts here. First, we don't use the term "else" with guards, we use "otherwise". Second, each individual case line has its own =
sign, and there is not an =
sign for the whole expression. Your code won't compile if you try to write something like:
myGuardStatement :: Int -> Int
myGuardStatement a = -- BAD!
| a <= 2 ...
| a <= 6 ...
| otherwise = ...
Pattern Matching
Unlike other languages, Haskell has other ways of branching your code besides booleans. You can also perform pattern matching. This allows you to change the behavior of the code based on the structure of an object. For instance, we can write multiple versions of a function that each work on a particular pattern of arguments. Here's an example that behaves differently based on the type of list it receives.
myPatternFunction :: [Int] -> Int
myPatternFunction [a] = a + 3
myPatternFunction [a,b] = a + b + 1
myPatternFunction (1 : 2 : _) = 3
myPatternFunction (3 : 4 : _) = 7
myPatternFunction xs = length xs
The first example will match any list that consists of a single element. The second example will match any example with exactly two elements. The third example uses some concatenation syntax we're not familiar with yet. But it matches any list that starts with the elements 1 and 2. The next line matches any list that starts with 3 and 4. Then the final example will match all other lists.
It is important to note the ways in which the patterns bind values to names. In the first example, the single element of the list is bound to the name a
so we can use it in the expression. In the last example, the full list is bound to the name xs
, so we can take its length. Let's see each of these examples in action:
>> myPatternFunction [3]
6
>> myPatternFunction [1,2]
4
>> myPatternFunction [1,2,8,9]
3
>> myPatternFunction [3,4,1,2]
7
>> myPatternFunction [2,3,4,5,6]
5
Note that the order of our different statements matters! The second example could have also matched the (1 : 2 : _)
pattern. But since we listed the [1,2]
pattern first, it used that version of the function. If we put a catchall value first, our function will always use that pattern!
-- BAD! Function will always return 1!
myPatternFunction :: [Int] -> Int
myPatternFunction xs = 1
myPatternFunction [a] = a + 3
myPatternFunction [a,b] = a + b + 1
myPatternFunction (1 : 2 : _) = 3
myPatternFunction (3 : 4 : _) = 7
Luckily, the compiler will warn us by default about these un-used pattern matches:
>> :load MyFirstModule
MyFirstModule.hs:31:1: warning: [-Woverlapping-patterns]
Pattern match is redundant
In an equation for ‘myPatternFunction': myPatternFunction [a] = ...
MyFirstModule.hs:32:1: warning: [-Woverlapping-patterns]
Pattern match is redundant
In an equation for ‘myPatternFunction': myPatternFunction [a, b] = ...
MyFirstModule.hs:33:1: warning: [-Woverlapping-patterns]
Pattern match is redundant
In an equation for ‘myPatternFunction': myPatternFunction (1 : 2 : _) = ...
MyFirstModule.hs:34:1: warning: [-Woverlapping-patterns]
Pattern match is redundant
In an equation for ‘myPatternFunction': myPatternFunction (3 : 4 : _) = ...
As a final note, an underscore (like we see above) can be used for any pattern or part of a pattern that we don't need to use. It functions as a catchall and works for any value:
myPatternFunction _ = 1
Case Statements
You can also use pattern matching in the middle of a function with case statements. We could rewrite the previous example like so:
myCaseFunction :: [Int] -> Int
myCaseFunction xs = case xs of
[a] -> a + 3
[a,b] -> a + b + 1
(1 : 2 : _) -> 3
(3 : 4 : _) -> 7
xs -> length xs
Note that we use an arrow ->
instead of an equals sign for each case. The case statement is a bit more general in that you can use it deeper within a function. For instance:
myCaseFunction :: Bool -> [Int] -> Int
myCaseFunction usePattern xs = if not usePattern
then length xs
else case xs of
[a] -> a + 3
[a,b] -> a + b + 1
(1 : 2 : _) -> 3
(3 : 4 : _) -> 7
_ -> 1
Where and Let
So if you come from a background in a more imperative language, you might be making an observation right now. You might notice that we never seem to define intermediate variables. All the expressions we use come from the patterns of the arguments. Now, Haskell doesn't technically have "variables", because expressions never change their value! But we can still define sub-expressions within our functions. There are a couple different ways to do this. Let's consider one example where we perform several math operations on some inputs:
mathFunction :: Int -> Int -> Int -> Int
mathFunction a b c = (c - a) + (b - a) + (a * b * c) + a
While we can congratulate ourselves on getting our function on one line, this code isn't actually very readable. We can make it far more readable by using intermediate expressions. We'll first do this using a where
clause.
mathFunctionWhere :: Int -> Int -> Int -> Int
mathFunctionWhere a b c = diff1 + diff2 + prod + a
where
diff1 = c - a
diff2 = b - a
prod = a * b * c
The where
section declares diff1
, diff2
, and diff3
as intermediate values. Then we can use them in the base of the function. We can use where
results within each other, and it doesn't matter what order we list them in.
mathFunctionWhere :: Int -> Int -> Int -> Int
mathFunctionWhere a b c = diff1 + diff2 + prod + a
where
prod = diff2 * b * c
diff1 = c - a
diff2 = b - diff1
However, be sure you don't make a loop by making your where results depend on each other!
mathFunctionWhere :: Int -> Int -> Int -> Int
mathFunctionWhere a b c = diff1 + diff2 + prod + a
where
diff1 = c - diff2
diff2 = b - diff1 -- BAD! This will cause an infinite loop!
-- diff1 depends on diff2!
prod = a * b * c
We can also accomplish the same result by using a let
statement. This is a similar syntactic construct, except we declare the new expressions beforehand. We then have to use the keyword in
to signal the expression that will use the values.
mathFunctionLet :: Int -> Int -> Int -> Int
mathFunctionLet a b c =
let diff1 = c - a
diff2 = b - a
prod = a * b * c
in diff1 + diff2 + prod + a
In IO
situations like we had when printing and reading input, you can use let
as an action without needing in
. You actually need to do this instead of using where
when your expression depends on the user's input:
main :: IO ()
main = do
input <- getLine
let repeated = replicate 3 input
print repeated
We can get around this though. We can use where
to declare functions within our functions! The example above could also be written like so:
main :: IO ()
main = do
input <- getLine
print (repeatFunction input)
where
repeatFunction xs = replicate 3 xs
In this example, we declare repeatFunction
as a function that takes a list (or a String in our case!). Then on the print line, we pass our input string as an argument to the function. Cool!
Summary
This concludes part 2 of our Haskell Liftoff series. We covered a lot of ground here! We started writing our own code, getting user input, printing to the terminal, and running our Haskell as an executable. Then learned about some more advanced function syntax. We explored if-statements, pattern matching, where and let clauses.
If you think some of this was a little confusing, don't be afraid to go back and check out part 1 to solidify your knowledge on types and expressions! If you're good on this material, you should now move on to part 3. There we'll discuss the various ways of creating our own data types in Haskell!
If you want to take a look at some different beginner resources and neat tools, check out our Beginner's Checklist! It will also provide you with a quick review of this whole series!
If you're starting to feel confident enough to want to start your own Haskell project (even a small one!), you should also take a look at our Stack Mini Course! It will walk you through using the Stack utility to create a project. You'll also learn to add components and incorporate Haskell's awesome set of open source libraries into your code!