TDA555 - Introduction to functional programming

Looking up documentation

An important part of programming in any language is knowing how to find documentation on the functions you're using. When using Haskell, hoogle is by far the most useful tool for this.

In hoogle you can search for function names to find their documentation. But sometimes it can be more useful to search for the type of the function you want. Hoogle is smart and will supply a more general function than what you are asking for, as long as it would still type-check.

Hoogle will forward you to the hackage documentation, which is a great place to learn new libraries. They usually have a reasonable readme (if they don't you probably should look at another library), and point towards tutorials helping you understand them. For example here is the hackage documentation of the wonderful library containers.

Debugging in Haskell

The computer does what you tell it to do, not what you expect it to do. This truth has haunted computer programmers of every language, and Haskell is no exception. So how do you avoid bugs in Haskell, and how do you solve them? Here are a few tips and tricks.

A note on print-debugging

In other languages, you might be used to the good old tactic of "print debugging", just sprinkle print statements in your code, and let it run. This tactic is far, far less useful in Haskell. It exists, see this, but it probably won't help you. This is the case since Haskell expressions do not execute in a sequential fashion like in imperative languages, but rather are evaluated when, and if needed.

Do not fret however, there are other ways to help debug your code in Haskell!

Debugging your functions in ghci

The most effective tool to help you in debugging Haskell is ghci. When writing Haskell code you should keep a window open with ghci at all times. While writing your function, experiment in ghci at the same time. Test every function you write with some basic examples to make sure they work as intended.

ghci does have a breakpoint debugger, but effective use of it requires thorough knowledge of lazy evaluation and even then is not all that useful.

Let the compiler do the heavy lifting

Haskell has one of the most advanced and useful type systems out there. Use it to your advantage! There are several ways for GHC to help you avoid bugs.

Supply types for your functions! Long before you start thinking about what steps your function should take, you should figure out what type you want your function to have. What does your input look like? And what does your output look like? Just by doing this step, by telling Haskell what function you are planning on writing you can avoid many errors.

As an example, say that you're writing a function for reading the contents of a file. And then want to use that function in main, and print the contents. One might try to write something like this:

getContents file = do
  contents <- readFile file
  putStrLn "Read file!"

main = do
  contents <- getContents "somefile.txt"
  print contents

[Task] Can you tell what is wrong? What will get printed do you think?

[Solution] main will always print () since that is the result of the call to putStrLn in getContents.

Now, had we gone about writing this function differently, by first figuring out that, what we want getContents to return is an IO String, then the compiler would have told us something was up.

getContents :: FilePath -> IO String
getContents file = do
  contents <- readFile file
  putStrLn "Read file!"
Debug.hs:4:3: error:
    • Couldn't match type ‘()’ with ‘[Char]’
      Expected: IO String
        Actual: IO ()
    • In a stmt of a 'do' block: putStrLn "Read file"
      In the expression:
        do contents <- readFile file
           putStrLn "Read file"
      In an equation for ‘Main.getContents’:
          Main.getContents file
            = do contents <- readFile file
                 putStrLn "Read file"
  |
4 |   putStrLn "Read file"
  |   ^^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.

GHC is telling us that it expected an IO String but got an IO (). Saying you haven't given me what you said you'd give me.

With the knowledge that GHC will at least correct you when you supply the wrong type. We can feel safer that this solution is correct:

getContents :: FilePath -> IO String
getContents file = do
  contents <- readFile file
  putStrLn "Read file!"
  return contents

Avoiding partial functions Pattern matching and recursion is the bread and butter of Haskell programming. Though by default Haskell does not give a compile time error whenever you don't cover all possible cases. Functions that can handle all their possible inputs are called total. Writing a total function is something admirable and writing partial (non-total) functions is a sin comparable to animal cruelty. Sadly we have some examples of sinful, partial functions in Prelude. Functions like:

head :: [a] -> a
head (x:_) = x

tail :: [a] -> [a]
tail (_:xs) = xs

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n-1)

These functions are all non-total since they don't handle all their possible inputs. Why is this a bad thing? Because almost always this signals an assumption that is not told to the compiler. And if you don't tell the compiler the assumptions you are making, the compiler cannot help you keep those assumptions true. For head and tail, this assumption would be "The input has to be non-empty", for factorial the assumption would be "the input is positive". You should strive to tell the compiler all assumptions you are making. In general, you can fix your non-total functions in two ways:

  1. Restrict the input type to only ones which always return valid cases, or
  2. Signal that your function does not always return values by using Maybe or Either in the output.

Which of these you prefer is up to you. In general, the first forces the function caller to make sure the input is correct. While the latter forces the caller to make sure the function didn't fail. There are situations where one or the other is more appropriate. What is never appropriate is writing a non-total function and just keeping track of the assumptions you're making on your own.

For example, head above could be fixed in two ways:

import Data.List.NonEmpty

head' :: NonEmpty a -> a
head' (x :| _) = x

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

There is a lot more to say about partial functions, if you're interested check out this blog post

Use QuickCheck

QuickCheck is neat. Very neat in fact. One of the things that makes QuickCheck very neat, is its ability to shrink problematic test cases. Where it tries to find an as simple/small as possible failing test case. Given, for example, the following code that splits a list into two.

split :: [a] -> ([a], [a])
split [] = ([],[])
split (x:y:zs) = (x:xs, y:ys)
  where
    (xs, ys) = split zs

Sadly the author of the code forgot to handle lists of length 1, and as such it will fail for all lists of odd length. Had we tested almost any property of split QuickCheck would have catched this case.

For example with the following property:

splitLengthsProp :: Arbitrary a => [a] -> Bool
splitLengthsProp xs = length (ys ++ zs) == length xs
  where
    (ys, zs) = split xs

Gives this result when QuickChecked:

ghci> quickCheck splitLengthsProp
*** Failed! (after 2 tests):
Exception:
  Factorial.hs:(4,1)-(7,23): Non-exhaustive patterns in function split
[()]

QuickCheck correctly found the singleton list case, pointing you in the right direction for how to solve the problem in your code!

I liked TDA555 and would like to learn more about Haskell!

Where do I go from here?

There is one known best way to learn programming: ✨ to write programs ✨. But I realize that sometimes creativity is lacking, and one can want someone to guide in what programs to write. Every December there is usually a wonderful advent calendar with small daily programming challenges called advent of code, start by doing them!

{{ begin .Data }} Data has a leaderboard for each year's advent of code! Join it here.

There are also a number of elective courses covering more functional programming. In no particular order:

  • Domain-specific language of maths
  • Advanced Functional Programming
  • Programming language technology
  • Compiler construction
  • Parallel functional programming
  • Types for programs and proofs

{{ end .Data }}