A

Setting up a simple Haskell project using Cabal.

Posted on November 17, 2024

A mini tutorial about how to use cabal to configure a new project.

I’m going to write a series solving the Advent Of Code 2024 problems and this blog entry will focus on how to configure a simple project using the cabal tool.

Assuming you already have cabal available to you (otherwise you can install it in multiple ways).

Cabal init.

Our first step is to create a new folder AdventOfCode, enter inside it and run cabal init with some options. (We assume you are using *nix operating system or wls in windows)

mkdir AdventOfCode
cd AdventOfCode
cabal init --libandexe --tests

In this case we choose the options --libandexe --tests, this mean that cabal would generate a project structure with tree things in mind:

This is the tree structure that the above command would generate:

.
├── AdventOfCode.cabal
├── app
│   └── Main.hs
├── CHANGELOG.md
├── MyLibTest.hs
└── src
    └── MyLib.hs

The file AdventOfCode.cabal contains the cabal configuration for the project. Is inside this file were you must add more files to the library/executable/tests for cabal and ghc to include them at compilation time.

With this you should be able to run the tree commands:

cabal build
cabal run
cabal test

They shouldn’t do much, but they also shouldn’t fail. If there is a error at this point you need to solve it before advancing further!

If you open the file AdventOfCode.cabal after the initial section with the project information, you can see a section like this:

library
    exposed-modules:  MyLib

    -- Modules included in this library but not exported.
    -- other-modules:

    -- LANGUAGE extensions used by modules in this package.
    -- other-extensions:
    build-depends:    base ^>=4.14.3.0
    hs-source-dirs:   src
    default-language: Haskell2010

You can refer to the cabal documentation for information about those and more fields, but let’s have an overview of them:

Then in the next section you should see something like:

executable AdventOfCode
    main-is:          Main.hs

    -- Modules included in this executable, other than Main.
    -- other-modules:

    -- LANGUAGE extensions used by modules in this package.
    -- other-extensions:
    build-depends:
        base ^>=4.14.3.0,
        AdventOfCode

    hs-source-dirs:   app
    default-language: Haskell2010

This section defines a executable whose name is AdventOfCode. It means that we can use cabal run AdventOfCode to run this executable.

We only see a new field here:

The main-is field is to specify the name of the module that contains a main function. This main function is the function that we are going to run when we do cabal run. It is an error if it’s not present. In our case it is setted to Main.hs, as we also have hs-source-dirs setted to app, this means that cabal expects to see the Main.hs file in the path app/Main.hs.

You may also notice the that build-depends has base (as before) and AdventOfCode. The meaning of this is that our library defined above is a dependence of our executable. This is, the executable can see all the exported modules of our library and use all the exported functions and types from it.

Finally, the test section.

test-suite AdventOfCode-test
    default-language: Haskell2010
    type:             exitcode-stdio-1.0

    -- Directories containing source files.
    -- hs-source-dirs:
    main-is:          MyLibTest.hs
    build-depends:    base ^>=4.14.3.0

In this case you should notice that it didn’t specify a hs-source-dirs, but it has a main-is and that cabal generated a file MyLibTest.hs at the root of the project. Usually you want to set the hs-source-dirs to something like tests and put the main file inside this folder.

The build-depends is for adding new libraries that your original library and executable may not need but that can simplify your life to write tests! This means that the users of this library won’t need those libraries whenever they use your library unless they want to run your library tests.

Modifying the default configuration

We are going to change the default structure of our project from:

.
├── AdventOfCode.cabal
├── app
│   └── Main.hs
├── CHANGELOG.md
├── MyLibTest.hs
└── src
    └── MyLib.hs

To:

.
├── AdventOfCode.cabal
├── app
│   └── Main.hs
├── CHANGELOG.md
├── tests/Main.hs
└── src
    └── Problem1.hs

And make the changes inside the files to reflect this change.

First we create the tests folder and move the MyLibTest.hs to it.

At the root of the project

mkdir tests
mv MyLibTest.hs  tests/Main.hs

Then we change the name of src/Mylib.hs to src/Problem1.hs

mv src/MyLib.hs  src/Problem1.hs

Now to tell cabal that we want those changes we need to update the fields.

The library section has to change the exposed-modules like this:

    exposed-modules:  Problem1

The test section needs to uncomment the hs-source-dirs and the section like:

    hs-source-dirs:  tests
    main-is: Main
    build-depends:    base ^>=4.14.3.0
    ,AdventOfCode

Now if you try any of the commands of cabal (run/build/test) you would find that all of them fail!

For example cabal build gave me:

Build profile: -w ghc-8.10.7 -O1
In order, the following will be built (use -v for more details):
 - AdventOfCode-0.1.0.0 (lib) (configuration changed)
 - AdventOfCode-0.1.0.0 (exe:AdventOfCode) (configuration changed)
Configuring library for AdventOfCode-0.1.0.0..
Preprocessing library for AdventOfCode-0.1.0.0..
Building library for AdventOfCode-0.1.0.0..

src/Problem1.hs:1:8: error:
    File name does not match module name:
    Saw: ‘MyLib’
    Expected: ‘Problem1’
  |
1 | module MyLib (someFunc) where
  |        ^^^^^
cabal: Failed to build AdventOfCode-0.1.0.0 (which is required by
exe:AdventOfCode from AdventOfCode-0.1.0.0).

We need to go to src/Problem1.hs and change the first line from:

module MyLib (someFunc) where

To:

module Problem1 (someFunc) where

Every file in Haskell is a module and the module we are declaring in this line must match the name of the file (or the path, more on that later)

If you try again the error changes to

app/Main.hs:3:1: error:
    Could not find moduleMyLib
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
3 | import qualified MyLib (someFunc)
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This is because the app/Main.hs file is importing the someFunc function from the old file src/MyLib, we need to change this.

If you open app/Main.hs it may look like

module Main where

import qualified MyLib (someFunc)

main :: IO ()
main = do
  Mylib.someFunc

We need to replace the MyLib for Problem1 in every place we find it, then we are going to see something like this:

module Main where

import qualified Problem1 (someFunc)

main :: IO ()
main = do
  Problem1.someFunc

After this there shouldn’t be more problems using cabal build or cabal run. However, you still need to change the occurrences of Mylib inside tests/Main.hs after that you shouldn’t have issues with cabal test.

The problem’s module structure.

The problems of advent of code always have two parts, to reflect this every problem we solve will have two functions: solve1 and solve2. The solve1 function would solve the original problem and the solve2 the modified one unblocked after solving the first problem.

However during the series we want to have two additional functions: solve1_2 and solve2_2. They also correspond to solutions of the first and second part of a problem, but they are intended to be for experimentation in the blogs.

If solve1 used a very naive way to do something then solve1_2 may use some advance technique that we are going to explore. But usually the functions suffixed with _2 will be used to explore how we should write the code assuming that we are in a big project with a team. This way you can begin to learn how to write code that others can read at work.

With this said we need to modify src/Problem1.hs like this:

module Problem1(solve1,solve1_2,solve2,solve2_2) where

solve1 :: IO ()
solve1 = undefined

solve1_2 :: IO ()
solve1_2 = undefined

solve2_1 :: IO ()
solve2_1 = undefined

solve2_2 :: IO ()
solve2_2 = undefined

Then under app/Main.hs we need to change the file like

module Main where

import qualified Problem1 (solve1,solve1_2,solve2_1,solve2_2) qualified as P1

main :: IO ()
main = do
  P1.solve1

Every time you want to run a particular solver, you only need to change the

main = do
  P1.solve1

To the appropriate solver.

Every time you want to add a new problem you need to crate a new src/ProblemN.hl with the same functions and add the import to app/Main.hs.

And that’s it! We are ready to began to solve the advent of code problems!

Note that we didn’t update the tests/Main.hs file, this mean that our test are broken, but we are not going to use them anyway, we are going to keep them broken unless we need them later in the series. We only included them to give a complete guide on how to configure a project!

From here you can:

The blog real structure of the project

Although this blog has a Cabal file it doesn’t follow completely the structure described here. Not only that but we also have a nix-flake file that we use to manage the project together with cabal. The use of nix mitigated some of the problems that Haskell had for a long time with dependencies (up to some degree). But some times you still have those problems.

I plan to write another entry where I talk about the particular configuration of the blog, but for now this is what you may want to know about the current cabal file.

I’m just at the beginning of writing the AOC2024 entries, but I think I would need to add a tests section for the problems (as sometimes I need to test some functions). But I’m still not convinced of the benefit of it.