Organizing Our Package!
To start off the new year, we're going to look at the process of creating and managing a new Haskell project. After learning the very basics of Haskell, this is one of the first problems you'll face when starting out. Over the next few weeks, we'll look at some programs we can use to manage our packages, such as Cabal, Stack, and Nix.
The first two of these options both use the .cabal
file format for the project specification file. So to start this series off, we're going to spend this week going through this format. We'll also discuss, at a high level, what a package consists of and how this format helps us specify it.
If you'd like to skip ahead a little and get to the business of writing a Haskell project, you should take our free Stack mini-course! It will teach you all the most basic commands you'll need to get your first Haskell project up and running. If you're completely new to Haskell, you should first get familiar with the basics of the language! Head to our Liftoff Series for help with that!
What is a Package Anyway?
The .cabal
file describes a single Haskell package. So before we understand this file or its format, we should understand what a package is. Generally speaking, a package has one of two goals.
- Provide library code that others can use within their own Haskell projects
- Create an executable program that others can run on their computers
In the first case, we'll usually want to publish our package on a repository (such as Hackage or Github) so that others can download it and use it. In the second case, we can allow others to download our code and compile it from source code. But we can also create the binary ourselves for certain platforms, and then publish it.
Our package might also include testing code that is internal to the project. This code allows us (or anyone using the source code) to verify the behavior of the code.
So our package contains some combination of these three different elements. The main job of the .cabal
file is to describe these elements: the source code files they use, their dependencies, and how they get compiled.
Any package has a single .cabal
file, which should bear the name of the package (e.g. MyPackage.cabal). And a .cabal
file should only correspond to a single package. That said, it is possible for a single project on your machine to contain many packages. Each sub-package would have its own .cabal
file. Armed with this knowledge, let's start exploring the different areas of the .cabal
file.
Metadata
The most basic part of the .cabal
file is the metadata section at the top. This contains information useful information about the package. For starters, it should have the project's name, as well as the version number, author name, and a maintainer email.
name: MyProject
version: 0.1.0.0
author: John Smith
maintainer: john@smith.com
It can also specify information like a license file and copyright owner. Then there are a couple other fields that tell the Cabal package manager how to build the package. These are the cabal-version
and the build-type
(usually Simple
).
license-file: LICENSE
copyright: Monday Morning Haskell 2020
build-type: Simple
cabal-version: >=1.10
The Library
Now the rest of our the .cabal
file will describe the different code elements of our project. The format for these sections are all similar but with a few tweaks. The library
section describes the library code for our project. That is, the code people would have access to when using our package as a dependency. It has a few important fields.
- The "exposed" modules tell us the public API for our library. Anyone using our library as a dependency can import these modules.
- "Other" modules are internal source files that other users shouldn't need. We can omit this if there are none.
- We also provide a list of "source" directories for our library code. Any module that lives in a sub-directory of one of these gets namespaced.
- We also need to specify dependencies. These are other packages, generally ones that live in Hackage, that our project depends on. We can provide various kinds of version constraints on these.
- Finally, there is a "default language" section. This either indicates Haskell2010, or Haskell1998. The latter has fewer language features and extensions. So for newer projects, you should almost always use Haskell2010.
Here's a sample library section:
library:
exposed-modules:
Runner
Router
Schema
other-modules:
Internal.OptionsParser
Internal.JsonParser
hs-source-dirs:
src
build-depends:
base >=4.9 && <4.10
default-language: Haskell2010
A couple other optional fields are ghc-options
and default-language-extensions
. The first specifies command line options for GHC that we want to include whenever we build our code. The second, specifies common language extensions to use by default. This way, we don't always need {-# LANGUAGE #-}
pragmas at the tops of all our files.
library:
...
ghc-options:
-Wall
-Werror
default-extensions:
OverloadedStrings
FlexibleContexts
The library is the most important section of the file, and probably the one you'll update the most. You'll need to update the build-depends
section each time you add a new dependency. And you'll update one of the modules
sections every time you make a new file. There are several other fields you can use for various circumstances, but these should be enough to get you started.
Executables
Now we can also provide different kinds of executable code with our package. This allows us to produce a binary program that others can run. We specify such a binary in an "executable" section. Executables have similar fields, such as the source directory, language , and dependency list.
Instead of listing exposed modules and other modules, we specify one file as the main-is
for running the program. This file should contain a main
expression of type IO ()
that gets run when executing the binary.
The executable can (and generally should) import our library as a dependency. We use the name of our project in the build-depends
section to indicate this.
exectuable run-my-project
main-is: RunProject.hs
hs-source-dirs: app
build-depends:
base >=4.9 && <4.10
, MyProject
default-language: Haskell2010
As we explore different package managers, we'll see how we can build and run these executables.
Test Suites
There are a couple special types of executables we can make as well. Test suites and benchmarks allow us to test out our code in different ways. Their format is almost identical to executables. You would use the word test-suite
or benchmark
instead of executable
. Then you must also provide a type
, describing how the program should exit if the test fails. In most cases, you'll want to use exitcode-stdio-1.0
.
test-suite test-my-project
type: exitcode-stdio-1.0
main-is: Test.hs
hs-source-dirs: test
build-depends:
base >=4.9 && <4.10
, hunit
, MyProject
default-language: Haskell2010
As we mentioned, a test suite is essentially a special type of executable. Thus, it will have a "main" module with a main :: IO ()
expression. Any testing library will have some kind of special main
function that allows us to create test cases. Then we'll have some kind of special test command as part of our package manager that will run our test suites.
Conclusion
That wraps up the main sections of our .cabal
file. Knowing the different pieces of our project is the first step towards running our code. Next week, we'll start exploring the different tools that will make use of this project description. We'll start with Cabal, and then move onto Stack.