Experiences with support of Cabal's public sublibrary feature

This is not a very high quality post: I don’t introduce anything that hasn’t been written about before already, and I don’t feel like I looked very deeply into the subject. It’s just an experience report, but hopefully that experience is useful to someone out there.

Pre-ramble

This is a brief rundown of the state of Cabal’s “public sublibrary” feature from the perspective of a me. I normally develop Haskell projects within a Nix shell. The level of reproducibility is extremely attractive. I recently span out a few libraries I’ve developed into their own open-source repositories; see the flurry of activity (not to be mistaken for productivity; mv did most of the work) on my GitLab.

For streaming-json I think the use-case of it is for web servers which produce large amounts of JSON. It’s structured in a way that makes it easier to do less allocation while producing said JSON, just writing to handles directly. There are a lot of web server libraries in the Haskell ecosystem and it’d be nice to support as many of them as I can.

The standard way to accomplish this is to just make separate Cabal packages for each interop library; they can all live in harmony in monorepo. When you have your Git repo cloned somewhere, you ultimately have a directory structure that looks like this:

~/Code/streaming-json/.git
~/Code/streaming-json/LICENSE
~/Code/streaming-json/README
~/Code/streaming-json/streaming-json/streaming-json.cabal
~/Code/streaming-json/streaming-json/src/Streaming/JSON.hs
~/Code/streaming-json/streaming-json/src/Streaming/JSON/Lines.hs
~/Code/streaming-json/streaming-json-servant/streaming-json-servant.cabal
~/Code/streaming-json/streaming-json-servant/src/Streaming/JSON/Servant.hs
~/Code/streaming-json/streaming-json-yesod/streaming-json-yesod.cabal
~/Code/streaming-json/streaming-json-yesod/src/Streaming/JSON/Yesod.hs
...
# haha, "streaming-json/streaming-json/streaming-json"

Anyway, it’s sorta crap. I don’t want to maintain 3 or more almost-identical .cabal files, in 3 or more different Hackage packages, in 3 or more almost-identical source trees, with the inability to see anything other than streaming-json in any of the paths at a glance 1. It’s a bunch of minor irritations, but I just don’t like dealing with a bunch of minor irritations every time I switch files within a project in my editor.

In Cabal we don’t have feature flags like in Cargo. Feature flags undoubtedly do have their own potential pitfalls, discussed at some length across a few different GitHub issues, Reddit posts, and Discourse forums. Proponents such as myself are likely to think: well, Cargo does have them – and with very little actual restriction in terms of the foottrebuchets you can erect, and it seems to work quite nicely. But I could very well be blissfully ignorant of the pain points feature flags perhaps are creating for Rust/Cargo ecosystem maintainers. And definitely I am blissfully ignorant of the pain it would create for the Cabal project to actually implement.

Happily, Cabal does have another trick up its sleeve: public sublibraries. This was written about already by Kowainik back in 2019 – so how are things fairing almost 6 years later?

Recap: You can write sublibraries as ordinary library stanzas in your cabal file, just with an additional name, like library streaming-json-servant. Then set visibility: public within that stanza, and now you have a public sublibrary. If it’s in your available package set, then you should be able to then use that library from another package, like build-deps: streaming-json:streaming-json-servant.

This seems like a great way to implement optional features (with their own transitively-optional dependencies) in a library because unlike Cargo feature flags, which merely “should” be additive, Cabal sublibraries are additive 2. So that footgun is avoided completely.

Getting to a REPL from various toolchain setup options

I’ve now tested out the feature across a few different “typical” Haskell development setups. The goal is to use a the streaming-json library and its streaming-json-servant sublibrary from another project, fetching streaming-json from its Git repo.

I set up a repo on my GitLab called sublib-test. Each branch has a different setup from which I try to pull in streaming-json and its sublibrary. There’s not any “actual” code there. (I feel no offence if readers just flick through the branches on that repo to see what’s different in each, rather than reading on; a repo speaks a thousand words.)

A few of the setups use Nix. You could set up a Nix project without using flakes at all, but use Nix flakes I have. I like the ergonomics of being able to nix flake update my dependencies, although you can definitely accomplish that with niv and its relatives too. In any case, the “real” difference between usual Nix setups for Haskell projects is the choice of using the default Haskell infra in Nixpkgs, or to use IOHK’s haskell.nix – in both style and substance they are quite different. The third option is using Nix just for getting a development shell going, which is fine, but nearly equivalent to using ghcup, except with better isolation of different toolchains across projects.

Basic scaffold

See the link in the heading. The branches on this repo are the different options below.

Option 1: Nix flake

  1. Use nix flake init -t templates#haskell-hello to get the basic scaffold
  2. Do some very minor finagling to get the flake.nix to support “extra deps” from outside Hackage.
  3. Add the Git repo as a input to the flake.nix or use fetchFromGitLab. I put it as an input on the flake.nix (with flake = false), because it’s nice to be able to nix flake update a dependency.
  4. nix develop (I would recommend Direnv’s use_flake in an actual project. You will likely get better editor integration [such as if your dev shell provides HLS, cabal-gild, fourmolu, etc.] that way.)
  5. cabal repl
  6. Realise that it doesn’t work and experience slight despair and confusion – what is a package anyway, as opposed to a component, or sublibrary? Are these distinctions the reason why sublibrary support is kinda broken and complex, because you need different code to handle all the different sorts of “components” that Cabal packages contain? Can you convince both Nix and Cabal of the existence of the sublibrary somehow? The answer to these questions are I don’t know; nor do I know if they are the right questions in the first place. ghc-pkg list seems to pick up the sublibrary.

Option 2: haskell-flake based on nix flake init -t templates#haskell-flake

Basically the same as plain but in a nice, declarative wrapper around plain Nix, which I’m definitely coming around to. My experience with nice declarative abstraction in Nix has sometimes involved having to completely break out of the nice declarative abstraction in order to do something though, so I’m slightly weary of it. Sublibrary support isn’t any different here from in the plain option.

Option 3: haskell-nix based on nix flake init -t templates#haskell-nix

This uses haskell.nix to parse the cabal.project file (see below). I almost gave up after 4 hours of being stuck on compiling generics-sop-lib-generics-sop-x86_64-w64-mingw32 until I realised it’s configured in nix/hix.nix where cross platform compilation support is configured (which is pretty cool to have! But not very useful for my development environment).

An initial cabal repl actually works within this environment – but only after cabal-install itself fetched streaming-json. For actual builds (as opposed to generating a dev shell) haskell.nix does actually fetch the repository, and it’s able to build and utilise the sublibrary. Huzzah – there’s at least one way for a Haskell Nix project to use Cabal sublibraries. I’m not sure how I feel about it overall, as it’s a quite complex abstraction, with its own nixpkgs even.

Option 4: ghcup + Cabal setup

This is what newcomers to Haskell would likely be using. The experience here was actually very straightforward.

  1. Use ghcup to set up GHC and cabal-install.
  2. Add a cabal.project which has the Git repo for streaming-json at the right revision.
  3. cabal update
  4. cabal repl
  5. >>> import Streaming.JSON.Servant - yay!

Option 5: Stack

This was also very straightforward and a likely option that Haskell newcomers would use. Stack makes a bit more effort to have helpful (and searchable) error messages which was nice.

  1. Use ghcup to acquire Stack.
  2. Add a stack.yaml
  3. stack repl
  4. >>> import Streaming.JSON.Servant - yay!

Documentation

This is where sublibrary support still sucks: Haddock doesn’t generate documentation for sublibraries by default, but you can get some documentation out of it, by explicitly targetting all the components you want docs for, like cabal haddock streaming-json streaming-json:streaming-json-servant. The sublibrary documentation will be placed under an enigmatically-named directory l/sublibrary-name. I don’t think Hackage will do this for you at the moment though, so any sublibraries actually on there won’t get rendered docs AFAIK.

Conclusion

So there you have it. Support for public sublibraries is good for cabal-install, Stack, and Haskell.nix users. It’s sorta shit in “plain” Nix. This post was generated using the help of a keyboard and my tired fingers. Hopefully it is helpful to someone out there.


  1. my use of (projectile-find-file) is utterly enfeebled by this directory structure :D↩︎

  2. to the extent that Haskell themselves modules are additive, anyway. You could do slightly evil fake conditional compilation with an argumentless open type family type family Evil :: Symbol with no instance, but lots of definitions like x :: (Evil ~ "yes") => (); y :: (Evil ~ "no") => ()~, effectively locking the API to only what the ultimate choice of the Evil type is.↩︎

© Michael Ledger 2011-2026