Understanding Horizon Haskell (Part 1)

Note: A previous iteration of this blog post included a method for using horizon.dhall files to override dependencies locally. This explanation has been removed because it was giving the impression that horizon has any opinion on how your project should be scaffolded. This is not the case. Horizon package sets are api compatible with nixpkgs and make no assumptions as to your project structure.

This is the first in a series of blog posts on horizon haskell. In this first post, we’re going to explain what Horizon is, what problem it solves, and how to get set up using one of the open source package sets.

What is Horizon Haskell?

Horizon Haskell is a release management and stable package set toolkit for Haskell on nix. Horizon provides command line tools for managing stable package sets - collections of particular package versions that all build together.

Stable Package Sets (such as those provided by stackage), are metadata that points to particular versions of package data hosted elsewhere, usually on package repositories such stackage. Stack is able to refer to one of these stable package sets by name, such as lts-18.23 or nightly-2023-01-01, a manifest that supplied this metadata as a single unit. This data can be selectively overriden by the local project, allowing you to reuse the bulk of the stable package but with modifications. Stable package set data is incredibly useful for quickly bootstrapping projects without worrying about dependency conflicts.

Horizon Haskell aims to make stable package set data first-class and programmable, which offers a number of benefits: Package sets are projects in their own right, and can be hosted and versioned independently of the projects they contain or that depend on them. They can be manipulated algebraically and checked for properties, and the sources of package repository information is extensible and not limited to git or hackage. A single organisation can host multiple package sets, with different purposes and policies, so that a solution to dependency conflicts can be exactly shared between projects and does not need to be solved twice. Horizon metadata is entered in dhall and compiled directly to nix, so that the package set can be used as a flake input with minimal boilerplate, and client projects are built with nix.

Stable Package Sets are rich sources of breakage information - especially across large teams (such as the entire world). They can alert people to when upgrading a project is likely to break reverse dependencies they don’t know about. A project ‘X’ is a reverse dependency of a project ‘Y’ when ‘X’ has ‘Y’ as a depdencency. If someone needs to update ‘Y’ because a new version of ‘X’ requires it, they can update it in the stable package set to know exactly what liabilities they are placing on the rest of the organisation. The organisation can then decide what priorities to take: Do we delay the upgrade of ‘X’, and wait until the entire package set can handle the new version of ‘Y’, or do we force through the upgrade of ‘X’ and ‘Y’, and kick out any reverse dependencies that break as a result. This decision we will call a reverse dependency policy.

Of course, all haskell packages are reverse dependencies of GHC, which makes the policy around compiler upgrades one of the most important aspects of any package set’s CI. How many package should be forced to support the new compiler before moving ahead with the upgrade, and how many are we OK with being kicked out of the package set?

At the time of writing, Horizon Haskell offers four publicly available stable package sets, with different reverse dependency policies.

  • horizon-core - Tracks GHC as closely to master as possible, but horizon-gen-nix must continue to build. (Currently 9.7 master)

  • horizon-platform - Tracks the latest stable GHC as closely as possible, but horizon-gen-nix, pandoc, persistent, beam, servant, and postgresql-simple must continue to build. (Currently 9.4.4)

  • horizon-plutus - Tracks the latest stable GHC as closely as possible, but plutus must continue to build. (Currently 9.2.5)

  • horizon-wave-ocean-platform Tracks the latest stable GHC as closely as possible, but plutus-apps must continue to build. (Currently 8.10.7)

Later in this series we’ll see how to fork and support our own self-hosted package sets, but for now let’s see how we can use one of the publicly available package sets to build a project.

Setting Up

You can use any of the templates here to get started. Use nix flake show to see the available templates.

nix flake show git+https://gitlab.homotopic.tech/horizon/horizon-templates
└───templates
    ├───core: template: A template using horizon-core
    ├───danstack: template: A template using polysemy and composite
    ├───default: template: A template with nothing in it
    ├───minimal: template: A template with nothing in it
    ├───plutus: template: A template for plutus on-chain code
    └───wave-ocean: template: A template for plutus-apps

We will start with the minimal template, using horizon-platform as its package set (currently on 9.4.4 with around 1000 packages).

mkdir horizon-template
cd horizon-template
nix flake init -t 'git+https://gitlab.homotopic.tech/horizon/horizon-templates#minimal'

This will create a basic haskell app, lib and tests, cabal scaffolding, a flake.nix and a horizon.dhall.

A simple nix build will build the haskell project.

If you want incremental development, you can use cabal in a devShell.

nix develop
cabal build

That’s it. You can now add dependencies to your project and if they are in the horizon-platform package set they will be included as normal. Remember to relaunch the shell when you add them so they can be caught by nix.

If you need to add a dependency that is not in the project, or do a local dependency override - continue on.

Overriding dependencies

Adding or overriding dependencies locally can be done with IFD.

Let’s say we want a particular version of lens, “5.1.1”. First let’s make sure we add lens to the dependencies of our library in horizon-minimal-template.cabal, so we can see the changes.

  build-depends:
     base >=4.7 && <5
   , lens

One way to express the package set override is by changing the overlay expression in `flake.nix’ and add the package there. (It’s an old version of lens, so we’ll have to jailbreak it).

myOverlay = final: prev: {
    lens = doJailbreak (final.callHackage "lens" "5.1.1" { });
    horizon-minimal-template = final.callCabal2nix "horizon-minimal-template" ./. { };
  }

This will use nix’s callHackage function to override the version of lens in the package set. We we can check that this was overriden correctly by running nix build.

Later in the series we will look at ways we can produce our own package sets, and different options horizon supports for manipulating the compiled nix expressions. We’ll also look at the roadmap for the horizon toolkit. Thanks for reading.

Contact Us
To get in touch, use any of the contact details below.
@homotopic.tech
@locallycompact
Email: dan.firth@homotopic.tech
Phone: +447853047347