Modules and Function Syntax

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!