Hacking Haskell with Nix: Two Tricks for Enhanced Development
Haskell, with its strong type system and emphasis on purity, provides a robust foundation for building reliable software. Nix, a powerful package manager, offers reproducibility and dependency management. Combining these two technologies can create a development environment that is both robust and reproducible. However, integrating them seamlessly can sometimes present challenges. This blog post explores two practical tricks for “hacking” Haskell with Nix, significantly enhancing your development workflow. We’ll cover:
- Leveraging Nix Overlays for Package Customization: How to modify Haskell packages directly within your Nix environment without directly altering global configurations. This is crucial for experimenting with patches, adding dependencies that might not be officially available, or pinning specific package versions.
- Creating a Development Shell with `ghci`: How to build a development shell using `nix-shell` that pre-loads `ghci` with your project’s dependencies and provides a consistent REPL environment for rapid prototyping and testing. This will eliminate dependency hell and make it easy to reproduce the same environment across different machines.
Why Hack Haskell with Nix?
Before diving into the tricks, let’s briefly discuss why you might want to combine Haskell and Nix:
- Reproducibility: Nix guarantees that your Haskell project will build consistently across different machines and over time. This is invaluable for long-term maintainability and collaboration.
- Dependency Management: Nix handles dependencies with precision, resolving conflicts and ensuring that the correct versions of libraries are used. Say goodbye to “it works on my machine” issues.
- Isolation: Nix isolates your Haskell project’s dependencies from your system’s global environment. This prevents conflicts with other projects and ensures a clean build process.
- Experimentation: Nix allows you to easily experiment with different versions of Haskell packages and compilers without affecting your system’s stability.
These benefits make Nix a powerful tool for Haskell developers, especially those working on complex or long-lived projects.
Trick #1: Nix Overlays for Package Customization
One of the most powerful features of Nix is its ability to use overlays. Overlays allow you to override and customize packages without modifying the core Nixpkgs repository. This is incredibly useful for Haskell development, allowing you to:
- Apply patches to Haskell packages.
- Add dependencies that are not yet in Nixpkgs.
- Pin specific versions of packages.
- Modify build flags.
Understanding Nix Overlays
An overlay is essentially a function that takes the current Nixpkgs set as input and returns a new set with modifications. This function can modify existing packages, add new packages, or remove existing packages.
Here’s a basic structure of a Nix overlay:
“`nix
self: super: {
# Your customizations here
}
“`
self
refers to the modified Nixpkgs set that the overlay is creating. super
refers to the original, unmodified Nixpkgs set.
Example: Patching a Haskell Package
Let’s say you need to apply a patch to the attoparsec
Haskell package. You’ve found a bug and have a patch file, attoparsec.patch
, that fixes it. Here’s how you can use an overlay to apply this patch:
- Create an Overlay File (e.g., `overlay.nix`):
“`nix
self: super: {
haskellPackages = super.haskellPackages.override {
overrides = self: super: {
attoparsec = super.attoparsec.overrideAttrs (oldAttrs: rec {
patches = (oldAttrs.patches or []) ++ [ ./attoparsec.patch ];
});
};
};
}
“`Explanation:
haskellPackages = super.haskellPackages.override { ... }
: This line overrides thehaskellPackages
set within Nixpkgs. We’re usingoverride
because we want to modify its contents, not replace it entirely.overrides = self: super: { ... }
: This is another layer of overriding, specifically for packages withinhaskellPackages
. This allows us to target individual packages more easily.attoparsec = super.attoparsec.overrideAttrs (oldAttrs: rec { ... })
: This is where the magic happens. We’re overriding the attributes of theattoparsec
package.overrideAttrs
allows us to modify the existing attributes without having to rewrite them all.patches = (oldAttrs.patches or []) ++ [ ./attoparsec.patch ]
: This line adds our patch file to the list of patches that are applied to theattoparsec
package. The(oldAttrs.patches or [])
ensures that if the package already has patches, we append to the existing list instead of overwriting it.
- Reference the Overlay in your `default.nix` or `shell.nix`:
To use the overlay, you need to reference it in your project’s `default.nix` or `shell.nix` file. Here’s an example `shell.nix`:
“`nix
let
pkgs = import{
config = { allowUnfree = true; }; # Optional: Allow unfree software
overlays = [ ./overlay.nix ];
};
haskellPackages = pkgs.haskellPackages;
in
pkgs.mkShell {
buildInputs = [
haskellPackages.ghc
haskellPackages.cabal-install
haskellPackages.attoparsec # Uses the patched version
];
shellHook = ”
echo “Haskell development environment with patched attoparsec”
”;
}
“`Explanation:
overlays = [ ./overlay.nix ]
: This tells Nix to apply the overlay defined in `overlay.nix` when building the environment.haskellPackages.attoparsec
: This refers to the patched version of `attoparsec` defined in the overlay.
- Build the Environment:
Run
nix-shell
in the directory containing your `shell.nix` file. This will create a shell with the patchedattoparsec
package. - Verify the Patch:
To verify that the patch was applied, you can run a test that exposes the bug fixed by the patch within the
nix-shell
environment.
Example: Adding a Dependency Not in Nixpkgs
Sometimes, you might need to use a Haskell package that is not yet available in Nixpkgs. You can add it using an overlay. This often involves packaging the Haskell library as a Nix package.
Let’s assume we want to add a fictional Haskell package called foo-bar
, and we have a Cabal file for it. The simplest way to integrate it is by creating a custom package definition:
- Create a Package Definition (`pkgs/foo-bar/default.nix`):
“`nix
{ mkDerivation, base, Cabal, ghc }:mkDerivation {
pname = “foo-bar”;
version = “1.0.0”;
src = ./.; # Assuming the cabal file is in the same directory
buildDepends = [ base ];
buildSystem = Cabal;
meta = {
description = “A fictional Haskell package”;
license = “BSD3”; # Replace with the correct license
};
}
“`Explanation:
pname = "foo-bar";
: Package name.version = "1.0.0";
: Package version.src = ./.;
: Source location; replace `./.` with the path to the directory containing the Cabal file, if needed.buildDepends = [ base ];
: Dependencies.buildSystem = Cabal;
: Build system.
- Create the Overlay:
“`nix
self: super: {
haskellPackages = super.haskellPackages.override {
overrides = self: super: {
foo-bar = self.callPackage ./pkgs/foo-bar { };
};
};
}
“`Explanation:
foo-bar = self.callPackage ./pkgs/foo-bar { };
: This line tells Nix to build thefoo-bar
package using the definition in `./pkgs/foo-bar/default.nix`.callPackage
is a function that takes a Nix file (our package definition) and an attribute set of overrides as arguments. In this case, we’re not passing any overrides, so the second argument is an empty attribute set:{ }
.
- Update `shell.nix` to Include the New Package:
“`nix
let
pkgs = import{
config = { allowUnfree = true; };
overlays = [ ./overlay.nix ];
};
haskellPackages = pkgs.haskellPackages;
in
pkgs.mkShell {
buildInputs = [
haskellPackages.ghc
haskellPackages.cabal-install
haskellPackages.foo-bar # Include the new package
];
shellHook = ”
echo “Haskell development environment with foo-bar”
”;
}
“`
Best Practices for Using Overlays
- Keep overlays small and focused: Avoid making large, monolithic overlays. Instead, create smaller, more manageable overlays that address specific needs.
- Document your overlays: Add comments to your overlay files to explain why you are making specific changes.
- Test your overlays: Ensure that your overlays work as expected by running tests within the Nix environment.
- Consider using `niv` to manage dependencies: `niv` helps manage the versions of Nixpkgs and other Nix sources used in your project, providing more reproducibility.
Trick #2: Creating a Development Shell with `ghci`
The interactive Haskell environment, ghci
, is invaluable for rapid prototyping and testing. Setting up a development shell with `ghci` and your project’s dependencies can significantly improve your workflow.
Nix makes it easy to create a reproducible development shell using the `nix-shell` command. This shell will have all the necessary dependencies pre-loaded, allowing you to start hacking immediately.
Creating a `shell.nix` File
Create a file named `shell.nix` in the root of your Haskell project. This file will define the development environment.
“`nix
let
pkgs = import
config = { allowUnfree = true; };
};
haskellPackages = pkgs.haskellPackages;
in
pkgs.mkShell {
buildInputs = [
haskellPackages.ghc
haskellPackages.cabal-install
haskellPackages.your-package # Your project’s package
# Add other dependencies here
];
shellHook = ”
echo “Haskell development environment ready!”
cabal update
cabal build
ghci
”;
}
“`
Explanation:
pkgs = import
: Imports the Nixpkgs repository.{ ... } config = { allowUnfree = true; };
is optional and allows the use of software that is not free.haskellPackages = pkgs.haskellPackages
: Shorthand for referencing the Haskell packages.buildInputs = [ ... ]
: Specifies the packages that should be available in the development shell. This includesghc
(the Glasgow Haskell Compiler),cabal-install
(the Cabal build tool), and your project’s Haskell package. Replaceyour-package
with the actual name of your project’s package. You can add any other dependencies your project needs to this list.shellHook = '' ... ''
: Defines commands that are executed when the shell is entered. In this case, it prints a message, updates the Cabal package list, builds the project, and then startsghci
.
Using the Development Shell
To enter the development shell, simply run the following command in the directory containing the `shell.nix` file:
“`bash
nix-shell
“`
This will create a shell with all the specified dependencies pre-loaded. `ghci` will automatically start, ready for you to load your modules and start experimenting.
Integrating with Cabal
The `shell.nix` file includes cabal update
and cabal build
in the shellHook
. This ensures that your project is built before `ghci` is started. However, you might want to customize the Cabal build process further.
Here are some common customizations:
- Specifying a Cabal Sandbox:
Using a Cabal sandbox can help isolate your project’s dependencies further. You can create a sandbox and activate it in the `shellHook`:
“`nix
shellHook = ”
echo “Haskell development environment ready!”
cabal sandbox init
cabal sandbox add-source .
cabal update
cabal build
ghci
”;
“` - Running Tests:
You can also run your project’s tests in the `shellHook` to ensure that your changes are working correctly:
“`nix
shellHook = ”
echo “Haskell development environment ready!”
cabal update
cabal build
cabal test
ghci
”;
“` - Custom Build Flags:
If you need to pass custom build flags to Cabal, you can include them in the `cabal build` command:
“`nix
shellHook = ”
echo “Haskell development environment ready!”
cabal update
cabal build –flag your-package:enable-feature
ghci
”;
“`
Example: Using `ghci` with a Custom Package
Let’s say you have a simple Haskell project with the following structure:
“`
my-project/
├── src/
│ └── MyModule.hs
├── my-project.cabal
└── shell.nix
“`
The `MyModule.hs` file might contain:
“`haskell
module MyModule where
greet :: String -> String
greet name = “Hello, ” ++ name ++ “!”
“`
The `my-project.cabal` file would define the project’s metadata and dependencies (omitted for brevity).
The `shell.nix` file would be:
“`nix
let
pkgs = import
config = { allowUnfree = true; };
};
haskellPackages = pkgs.haskellPackages;
in
pkgs.mkShell {
buildInputs = [
haskellPackages.ghc
haskellPackages.cabal-install
haskellPackages.my-project # Replace with the actual package name
];
shellHook = ”
echo “Haskell development environment ready!”
cabal update
cabal build
ghci
”;
}
“`
After running nix-shell
, you can load your module in `ghci` and start experimenting:
“`ghci
Prelude> :load src/MyModule.hs
[1 of 1] Compiling MyModule ( src/MyModule.hs, interpreted )
Ok, one module loaded.
*MyModule> greet “World”
“Hello, World!”
“`
Benefits of Using a Development Shell
- Consistency: Ensures that everyone working on the project has the same development environment.
- Isolation: Prevents conflicts with other projects and system-level dependencies.
- Reproducibility: Guarantees that the development environment can be recreated consistently over time.
- Ease of Use: Provides a simple and convenient way to start hacking on your Haskell project.
Conclusion
By leveraging Nix overlays and development shells, you can significantly enhance your Haskell development workflow. These two tricks provide a powerful combination of package customization, dependency management, and reproducibility. Experiment with these techniques to create a robust and efficient development environment for your Haskell projects.
Remember that Nix has a learning curve, but the investment in understanding its concepts and tools will pay off in the long run, especially for large or complex Haskell projects. Good luck and happy hacking!
Further Exploration
“`