Lesson 34. Organizing Haskell code with modules

After reading lesson 34, you’ll be able to

Up to this point in the book, we’ve covered a wide range of interesting topics related to Haskell. But we haven’t discussed one of the most basic topics: creating namespaces for your functions. Haskell uses a system of modules to create separate namespaces for functions and allow you to organize your code much better. This works similarly to modules in languages such as Ruby and Python, and to packages and namespaces in languages such as Java and C#.

You’ve already used Haskell’s module system. Every time you use import, you’re including a new module in your program. Additionally, all the built-in functions and types you have—such as [], length, and (:)—are all included in the standard module named Prelude that’s automatically imported. The full documentation for Prelude can be found on Hackage (https://hackage.haskell.org/package/base/docs/Prelude.html).

So far in this book, we’ve avoided modules by keeping all of your code in the same file and giving your functions unique names when there’s a conflict. In this lesson, you’ll start organizing your code correctly into modules. You’ll focus on a simple example: writing a command-line tool that prompts the user for a word and indicates whether the word is a palindrome. Ideally, you’d like to keep the main IO action in a separate file from the functions that work to determine whether text is a palindrome. This keeps your code better organized and makes it easier to extend your program with more code in the future. You’ll start by writing your program as a single file, and then correctly separating the code into two files.

Consider this

You have a type for books and a type for magazines. Each has the same field names, but they represent different things:

data Book = Book
   { title :: String
   , price :: Double }

data Magazine = Magazine
   { title :: String
   , price :: Double }

Both types are written using record syntax, which creates a problem. Record syntax automatically creates accessor functions title and price. Unfortunately, this causes an error because you’re attempting to define two functions of the same name. You want to avoid giving these fields such as bookTitle and bookPrice. How can you resolve this conflict?

34.1. What happens when you write a function with the same name as one in Prelude?

To get started, let’s create a better version of the default head function that you’ve been using throughout the book. In Prelude, head is defined as follows.

Listing 34.1. The definition in Prelude of head
head                    :: [a] -> a
head (x:_)              =  x
head []                 =  errorEmptyList "head"          1

The head function has a problem in that it throws an error when it’s applied to an empty list. This isn’t ideal for Haskell, and lesson 38 covers this in more detail when we talk about handling errors. The reason head throws an error is that there often isn’t a sensible value you can return. Languages such as Lisp and Scheme will return the empty list as the result of calling head on an empty list, but Haskell’s type system doesn’t allow this (because the empty list is usually a different type than values in the list itself). But you can come up with a solution to this problem if you constrain head to work on members of the Monoid type class. You’ll recall from lesson 17 that the Monoid type class is defined as follows.

Listing 34.2. The definition of the Monoid type class
class Monoid m where
    mempty :: m
    mappend :: m -> m -> m
    mconcat :: [m] -> m

All Monoids are required to define a mempty element. The mempty element represents the empty value for instances of Monoid. List is an instance of Monoid, and mempty is just the empty list, []. For members of Monoid, you can return the mempty element when you have an empty list. Here’s your new, safer version of head.

Listing 34.3. Oops, you accidentally created a function that already has a name!
head :: Monoid a => [a] -> a
head (x:xs) = x
head [] = mempty

If you write this code in a file, it’ll compile just fine, even though you’ve “accidentally” used the name of an existing function. That’s because the head you use all the time is part of the Prelude module. To test your new head function, you need an example of a list with values that are members of Monoid. In this case, you’ll use an empty list of lists (remember, the elements of your list must be an instance of Monoid).

Listing 34.4. An example list that’s a list of values that are an instance of Monoid
example :: [[Int]]
example = []

You can compile this code just fine, but something happens if you try to use head in GHCi. Because there’s already a function called head when you run this code in GHCi, you get an error that looks something like this:

Ambiguous occurrence 'head'                         1
It could refer to either 'Main.head'                2
defined at ...
or 'Prelude.head'                                   3

The problem is that Haskell doesn’t know which head you mean—the one defined in Prelude or the one you just wrote. What’s interesting is that the complaint is that there’s a Main.head function. When you don’t explicitly tell Haskell that you’re in a module, Haskell assumes that you’re the Main module. You can make this explicit by using the following line at the top of your file.

Listing 34.5. Explicitly defining a module for your code
module Main where                1

head :: Monoid a => [a] -> a
head (x:xs) = x
head [] = mempty

example :: [[Int]]
example = []

To specify precisely which head you mean, you can fully qualify your function’s name with the name of the module. You use Main.head to specify your head, and Prelude.head to use the default Prelude definition of head. Here’s an example in GHCi:

GHCi> Main.head example
[]
GHCi> Prelude.head example
*** Exception: Prelude.head: empty list

Next, you’ll expand on your use of modules to build a simple program that’s spread over two files.

Quick check 34.1

Q1:

Suppose you need to store the length of an object as a variable. For example:

length :: Int
length = 8

How would you use that value without conflicting with the existing length function in Prelude?

QC 34.1 answer

1:

You need to qualify the value as Main.length:

length :: Int
length = 8

doubleLength :: Int
doubleLength = Main.length * 2

 

34.2. Building a multifile program with modules

In this section, you’ll build a simple program that reads a line of user input and then indicates whether the word is a palindrome. You’ll start with a quick, single-file version of the program that can detect palindromes such as racecar but fails on Racecar! You’ll then refactor your code into two files, one dealing with the main program logic and the other a library to put all your code for properly detecting palindromes.

It’s generally good practice to separate groups of related functions into separate modules. The main module should primarily be concerned with the execution of your program. All of the logic for reasoning about palindromes should be kept in a separate file, because this makes it easier to keep track of the locations of library functions. Additionally, you can hide functions in a module the same way classes in Java or C# can have private methods. This allows you to have encapsulation so that only the functions you wish to export are available for use.

34.2.1. Creating the Main module

So far, you’ve been pretty casual with your filenames. Now that you’re starting to think about properly organizing code, you should be more careful. As you saw in unit 4, each Haskell program has a main function the same way that Java programs have a main method. Normally, you expect the main function to live in the Main module. Convention in Haskell is that modules should live in files of the same name as the module. When creating your palindrome project, you should start with a file named Main.hs. Here’s your program.

Listing 34.6. A first draft of your Main module
module Main where                                          1
isPalindrome :: String -> Bool                             2
isPalindrome text = text == reverse text

main :: IO ()                                              3
main = do
  print "Enter a word and I'll let you know if it's a palindrome!"
  text <- getLine
  let response = if isPalindrome text
                 then "it is!"
                 else "it's not!"
  print response

You can compile this program and test out your code or just load it into GHCi. You can see that your palindrome program isn’t as robust as you’d like:

GHCi> main
"Enter a word and I'll let you know if it's a palindrome!"
racecar
"it is!"
GHCi> main
"Enter a word and I'll let you know if it's a palindrome!"
A man, a plan, a canal: Panama!
"it's not!"

Your program correctly identifies racecar as a palindrome, but fails to identify A man, a plan, a canal: Panama! What you need is a bit of preprocessing for your strings to strip out whitespace, remove punctuation, and ignore case. In the past, you would just add this code to your file. But it makes sense to pull out your palindrome code into a separate file for two reasons. First, it makes your Main cleaner, and second, you can then more easily reuse your palindrome code in other programs.

34.2.2. Putting your improved isPalindrome code in its own module

You’ll put your palindrome code in a separate module. The module’s name will be Palindrome, so your code should be in a file named Palindrome.hs. Your Palindrome module will have a function, also named isPalindrome, which will be the function used by the Main module. You want to write a more robust version of isPalindrome so your module will also contain a series of helper functions: stripWhiteSpace, stripPunctuation, toLowerCase, and preprocess, which performs all of these. Here’s your full Palindrome.hs file.

Listing 34.7. The Palindrome.hs file
module Palindrome(isPalindrome) where                              1

import Data.Char (toLower,isSpace,isPunctuation)                   2

stripWhiteSpace :: String -> String                                3
stripWhiteSpace text = filter (not . isSpace) text

stripPunctuation :: String -> String
stripPunctuation text = filter (not . isPunctuation) text

toLowerCase :: String -> String
toLowerCase text = map toLower text

preprocess :: String -> String
preprocess = stripWhiteSpace . stripPunctuation . toLowerCase

isPalindrome :: String -> Bool
isPalindrome text = cleanText == reverse cleanText
  where cleanText = preProcess text

Let’s walk through this file step-by-step to get a better sense of what’s happening. You could’ve started your Palindrome function this way:

module Palindrome where

By default, this will export all the functions that you’re defining in Palindrome.hs. But you don’t want to export your helper functions; all you care about is isPalindrome. You can achieve this by listing all the functions you want to export in parentheses after the module name:

module Palindrome(isPalindrome) where

Here’s another way to format your export functions so that you can easily accommodate exporting additional functions:

module Palindrome
    ( isPalindrome
    ) where

Now the only function available from your Palindrome module is isPalindrome.

To create your helper functions, you need a few functions from the Data.Char module. In the past, you’ve crudely imported the entire module whenever you need to use one. But just as you can selectively export functions, you can also selectively import them. This import statement imports only the three functions you’ll need.

Listing 34.8. Importing only a specific subset of functions from Data.Char
import Data.Char (toLower,isSpace,isPunctuation)

The primary benefits of importing functions this way are that it improves readability and reduces the possibility that you’ll have an unexpected namespace collision when performing a nonqualified import.

Now the rest of your file is just like any other Haskell file you’ve used so far. All your helper functions are relatively self-explanatory.

Listing 34.9. The code in your module for properly detecting palindromes
stripWhiteSpace :: String -> String                                1
stripWhiteSpace text = filter (not . isSpace) text

stripPunctuation :: String -> String                               2
stripPunctuation text = filter (not . isPunctuation) text

toLowerCase :: String -> String                                    3
toLowerCase text = map toLower text

preprocess :: String -> String                                     4
preprocess = stripWhiteSpace . stripPunctuation . toLowerCase

isPalindrome :: String -> Bool                                     5
isPalindrome text = cleanText == reverse cleanText
  where cleanText = preprocess text

Your Palindrome module doesn’t have a main because it’s just a library of functions. Even without a main, you can still load your file into GHCi and test it out:

GHCi> isPalindrome "racecar"
True
GHCi> isPalindrome "A man, a plan, a canal: Panama!"
True

Now that you understand your Palindrome module, let’s go back and refactor your Main module.

Quick check 34.2

Q1:

Modify the module declaration so that you also export preprocess.

QC 34.2 answer

1:

module Palindrome(
                  isPalindrome
                 ,preprocess
                 ) where

 

34.2.3. Using your Palindrome module in your Main module

To use Palindrome, you add the import to your Main as you would any other module. As you’ll soon see, when your module is in the same directory as your Main, compiling your Main will automatically compile your other module.

Let’s suppose you’d like to keep your existing definition of isPalindrome in your Main. In the past, you’ve used import qualified Module as X to provide a named import for the modules you’d like to use (such as import qualified Data.Text as T). If you leave off the as X part of your qualified import, you use the name of the module itself to reference functions in that module. Here’s the start of your main refactored.

Listing 34.10. Performing a qualified import of your Palindrome module
module Main where
import qualified Palindrome

Now all you have left to do is change your call to isPalindrome to Palindrome.isPalindrome and you’re all set.

Listing 34.11. Using the qualified Palindrome.isPalindrome function
  let response = if Palindrome.isPalindrome text

Here’s your fully refactored Main.hs.

Listing 34.12. Your Main.hs file that uses your Palindrome.hs file
module Main where
import qualified Palindrome

isPalindrome :: String -> Bool
isPalindrome text = text == (reverse text)

main :: IO ()
main = do
  print "Enter a word and I'll let you know if it's a palindrome!"
  text <- getLine
  let response = if Palindrome.isPalindrome text
                 then "it is!"
                 else "it's not!"
  print response

Compiling this program is surprisingly simple. You can compile your Main.hs file with GHC and it’ll automatically find your module:

$ ghc Main.hs
[1 of 2] Compiling Palindrome       ( Palindrome.hs, Palindrome.o )
[2 of 2] Compiling Main             ( Main.hs, Main.o )
Linking Main ...

Finally, you can run your Main executable:

$ ./Main
"Enter a word and I'll let you know if it's a palindrome!"
A man, a plan, a canal, Panama!
"it is!"

This is a trivial case of having a simple module in the same directory. In the next lesson, you’ll explore stack, which is a powerful and popular build tool for Haskell. If you’re going to be building anything nontrivial, make sure you do it with stack. Nonetheless, it’s still helpful to understand how to compile multiple-file programs by hand.

Quick check 34.3

Q1:

You shouldn’t leave Main.isPalindrome there, as it’s no longer necessary. If you remove the code for Main.isPalindrome, how can you refactor your code so you no longer need to qualify Palindrome.isPalindrome?

QC 34.3 answer

1:

Change import qualified Palindrome to import Palindrome. Then remove Palindrome. from Palindrome.isPalindrome.

 

Summary

In this lesson, our goal was to teach you how to use modules to organize your Haskell programs. You learned how most of your programs automatically belong to a Main module. Next, you saw how to organize programs into separate files and compile them into a single program. You also learned how to export specific functions from your modules while hiding the rest. Let’s see if you got this.

Q34.1

Recall that in unit 4 we mentioned that Data.Text is strongly preferred over String for working with text data. Refactor this project to use Data.Text instead of String (in both the Main and Palindrome modules).

Q34.2

In unit 4, lesson 25, you wrote a program to “glitch” binary images. Revisit that program and pull out all the code specific to glitching images into its own Glitch module.