56
votes

If you declare a library + executable sections in a cabal file while avoiding double compilation of the library by putting the library into a hs-source-dirs directory, you cannot usually run your project with ghci and runhaskell anymore, especially if the executables have helper modules themselves.

What is a recommended project layout that

  • only builds what is needed once
  • allows using runhaskell
  • has a clean structure without hacks?
2

2 Answers

89
votes

Let's assume you have a mylib library, and mylib-commandline and mylib-server executables.

You use hs-source-dirs for the library and each executable so that each has their own project root, avoiding double compilation:

mylib/                      # Project root
  mylib.cabal
  src/                      # Root for the library
  tests/
  mylib-commandline/        # Root for the command line utility + helper modules
  mylib-server/             # Root for the web service + helper modules

Full directory layout:

mylib/                      # Project root
  mylib.cabal
  src/                      # Root for the library
    Web/
      Mylib.hs              # Main library module
      Mylib/
        ModuleA             # Mylib.ModuleA
        ModuleB             # Mylib.ModuleB
  tests/
    ...
  mylib-commandline/        # Root for the command line utility
    Main.hs                 # "module Main where" stub with "main = Web.Mylib.Commandline.Main.main"
    Web/
      Mylib/
        Commandline/
          Main.hs           # CLI entry point
          Arguments.hs      # Programm command line arguments parser
  mylib-server/             # Root for the web service
    Server.hs               # "module Main where" stub with "main = Web.Mylib.Server.Main.main"
    Web/
      Mylib/
        Server/
          Main.hs           # Server entry point
          Arguments.hs      # Server command line arguments parser

The stub-like entry point file mylib-commandline/Main.hs looks like this:

module Main where

import qualified Web.Mylib.Server.Main as MylibServer

main :: IO ()
main = MylibServer.main

You need them because an executable must start on a module simply called Main.

Your mylib.cabal looks like this:

library
  hs-source-dirs:   src
  exposed-modules:
    Web.Mylib
    Web.Mylib.ModuleA
    Web.Mylib.ModuleB
  build-depends:
      base >= 4 && <= 5
    , [other dependencies of the library]

executable mylib-commandline
  hs-source-dirs:   mylib-commandline
  main-is:          Main.hs
  other-modules:
    Web.Mylib.Commandline.Main
    Web.Mylib.Commandline.Arguments
  build-depends:
      base >= 4 && <= 5
    , mylib
    , [other depencencies for the CLI]

executable mylib-server
  hs-source-dirs:   mylib-server
  main-is:          Server.hs
  other-modules:
    Web.Mylib.Server.Main
  build-depends:
      base >= 4 && <= 5
    , mylib
    , warp >= X.X
    , [other dependencies for the server]

cabal build will build the library and the two executables without double compilation of the library, because each is in their own hs-source-dirs and the executables depend on the library.

You can still run the executables with runghc from your project root, using the -i switch to tell where it shall look for modules (using : as separator):

runhaskell -isrc:mylib-commandline mylib-commandline/Main.hs

runhaskell -isrc:mylib-server mylib-server/Server.hs

This way, you can have a clean layout, executables with helper modules, and everything still works with runhaskell/runghc and ghci. To avoid typing this flag repeatedly, you can add something similar to

:set -isrc:mylib-commandline:mylib-server

to your .ghci file.


Note that sometimes should split your code into separate packages, e.g. mylib, mylib-commandline and mylib-server.

3
votes

You can use cabal repl to start ghci with the configuration from the cabal file and cabal run to compile and run the executables. Unlike runhaskell and ghci, using cabal repl and cabal run also picks up dependencies from cabal sandboxes correctly.