Organizing Our Package!

cabal_file.jpg

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.

  1. Provide library code that others can use within their own Haskell projects
  2. 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.

  1. The "exposed" modules tell us the public API for our library. Anyone using our library as a dependency can import these modules.
  2. "Other" modules are internal source files that other users shouldn't need. We can omit this if there are none.
  3. 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.
  4. 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.
  5. 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.

Previous
Previous

Using Cabal on its Own

Next
Next

Happy New Years from MMH!