Search code examples
haskellcabalghcirunhaskell

How to make a Haskell cabal project with library+executables that still run with runhaskell/ghci?


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?

Solution

  • 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.