This is an example Haskell project using Nix to setup a development environment.
This uses Nix and Cabal without Stack. This is because when using Nix, you don't need Stack, as Nix provides harmonious snapshots of Haskell packages.
Stack users can still develop on this project, they just have to generate an appropriate stack.yaml
from the haskell-demo.cabal
file.
The first step is that we have to acquire cabal2nix
, which we use to generate a cabal.nix
file from the package.yaml
.
We also write a custom default.nix
then imports the cabal.nix
and adds extra custom build steps like encoding environment variables.
Both the cabal.nix
and default.nix
remain as Haskell callPackage
derivations.
This means the function parameter names may conflict with non-Haskell package names.
If you get a conflict, make sure to do explicit overrides when using callPackage
.
Note that the usage of package.yaml
means we are using the hpack format. This format is transformed to a cabal file via the hpack
command.
nix-shell -p cabal2nix
# using --hpack ensures that we always use package.yaml
cabal2nix --hpack . >./cabal.nix
The above command is also executed at the beginning of the shellHook
.
If this is the first time you've ran cabal
, then run cabal update
to get the latest package list in ~/.cabal
.
Building the package:
nix-build -E '(import ./pkgs.nix).haskellPackages.callPackage ./default.nix {}'
Building the releases:
nix-build --attr application
nix-build --attr applicationStrict
nix-build --attr docker
Install into Nix user profile:
nix-env -f ./release.nix --install --attr application
Install into Docker:
docker load --input "$(nix-build ./release.nix --attr docker)"
Run nix-shell
, and once you're inside, you can use:
# check the GHC version
ghc --version
# show all the packages that is registed with GHC
ghc-pkg list
# launch the repl
ghci
The cabal-install
package installs the cabal
command. This command and associated protocols is what defines what a Haskell package is. So even if you are using Stack, you are still creating Cabal packages. Note that cabal
configuration file is a ~/.cabal/config
.
To use cabal
, you need to generate the cabal file from the package.yaml
. You can do this by running hpack
. However this is also executed as part of the shellHook
.
At this point, you need to run cabal v2-configure
. This will create a dist
directory that will contain any build artifacts. This is also executed as part of the shellHook
.
It's important to read the guide for Cabal as this is information relevant to the Haskell ecosystem: https://www.haskell.org/cabal/users-guide/developing-packages.html
The most important commands are:
# this will launch GHCI for a given target
cabal v2-repl
# this will build the executable and library and put them into ./dist
cabal v2-build
# this will run the executable (you can pass the name of the executable)
cabal v2-run
# this will run the tests
cabal v2-test
# deletes ./dist
cabal v2-clean
# this will install the executable into the ~/.cabal/bin
cabal v2-install
Once you have finished developing, you can build the package using:
nix-build
Note that if you want to create a quick and dirty nix-shell
with GHC and a few packages, just use:
nix-shell -p 'ghc.ghcWithPackages (pkgs: [ pkgs.aeson pkgs.dlist ])'
# or if you want to specify a version
nix-shell -p 'haskell.packages.ghc865.ghcWithPackages (pkgs: [ pkgs.aeson pkgs.dlist ])'
Any module that is meant to be consumed as a library needs to be listed in the exposed-modules
. Any module that is not listed there are considered to be private modules.
The package.yaml
is also where you list new Haskell packages. For example if you want to get the algebraic-graphs
library, you basically use:
dependencies:
- base >= 4.7 && < 5
- algebraic-graphs >= 0.2 && < 0.3
Then you use cabal2nix
again and you re-enter the shell.
Note that Haskell dependency constraints and versions when using cabal2nix
is not determined by your package.yaml
, but instead by the Nixpkgs hash located in pkgs.nix
.
Remember that Haskell package versions conventionally use Major.Major.Minor.Patch
. For more information see: https://pvp.haskell.org/
For non-Haskell dependencies that are CLI executables, add them to:
system-build-tools:
- hello
They will be available during nix-build
and nix-shell
.
For them to be available for the output, you must use makeWrapper
.
For non-Haskell dependencies that are linkable libraries, add them to:
extra-libraries:
- mnl
- z
The name of these libraries is the suffix of the gcc
linking option:
-lmnl
giving youmnl
-lz
giving youz
for the zlib library.
They will be will available to nix-build
and nix-shell
. The cabal configure
will automatically find them and link them during compilation.
These non-Haskell dependencies must be explicitly named when using callPackage
so that they refer to the C libraries and not Haskell packages of the same name.
This is done in both shell.nix
and release.nix
.
For example:
haskellPackages.callPackage ./cabal.nix { hello = pkgs.hello; mnl = pkgs.libmnl; z = pkgs.zlib; }
The cabal v2-repl
only works against the build targets specified in the package.yaml
. You have to specify the target name:
# targets the library
cabal v2-repl haskell-demo
# targets the executable (which depends on the library)
cabal v2-repl haskell-demo-exe
# targets the tests (which depends on the library)
cabal v2-repl haskell-demo-test
However you need to understand how modules work in GHCi to use the REPL well. The documentation here explains the necessary commands: https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/ghci.html#what-s-really-in-scope-at-the-prompt
Basically remember these:
:browse
:show modules
:show imports
:module ModuleName
The cabal v2-repl target
command can be too heavy weight especially if you're modifying just 1 module in the entire codebase. And it can be problematic when you are breaking type signatures.
So instead use ghci ModuleA/ModuleB
to load up just that module and its dependencies.
This project also demonstrates how to use the FFI. C source files are located in csrc
, while the C headers are in include
.
The relevant attributes of package.yaml
are c-sources
, include-dirs
and install-includes
.
The c-sources
be a list of C files that need to be compiled into objects that are linked during compilation.
The include-dirs
is a list of directories containing C headers to be included. In this case, we have only pointed to include
because we are only using standard library headers and our own headers. But you can also point to system directories using absolute paths.
The install-includes
will ensure that these headers (relative to the include-dirs) are also exported to any downstream package that depends on this package. So they can make use of those same headers, if they were also writing their own C code.
Finally you just need to write code like FFI.hs
, and everything just works normally.
Use hlint
to lint your Haskell code which can suggest better ways of writing Haskell expressions.
hlint lint ./src ./app ./test
Hlint will give suggestions that aren't always relevant. In order to ignore these suggestions, they must be recorded like:
hlint lint ./src ./app ./test --default > ./.hlint.yaml
Use brittany
to automatically format your code.
find ./src ./app ./test -type f -name '*.hs' -print0 | xargs -0 -I{} sh -c 'brittany --check-mode "{}" || echo "{}"; exit 1'
In order to apply the formatting to each file:
brittany --write-mode inplace ./src/Demo.hs ./app/library/Main.hs
Then use git diff
to find the actual difference.
There is an orphaned branch postgres that shows how to integrate PostgreSQL into a Haskell project.
It contains a scripts
directory that contains executable scripts that initializes and starts and runs a local database. It also contains scripts that allow migration.
database-clean
database-destroy
database-init
database-migrate
database-start
database-status
database-stop
We bring in postgresql
and flyway
as additional dependencies into shell.nix
.
Then the migration files are put into migrations
directory.
See flyway documentation and postgresql documentation for more details.