Is it okay to use the cabal solver to conditionally define dependencies and instances like this?

Edit: In hindsight this approach will result in a circular dependency between alice and alice-bob-instances anyway so won’t work.

Edit 2: Also see my reply below where I admit that just having the orphan package is the best approach because anything else can cause breakages at a distance, but I still find that quite a bit unsatisfactory. I’m leaving my initial thoughts here though in case it provokes further discussion and/or approaches I haven’t considered.

Let’s say I have a package alice, and a datatype that can be an instance of a class in package bob, but otherwise alice does not depend on bob

The common approach is to define a library alice-bob-instance and define an orphan instance inside this package.

If people need the bob instance for alice they can just import alice-bob-instance.

The problem is, yes, this is an orphan instance, so wherever one wants to use the instance you’ll have to explicitly import the module it comes from which is a bit annoying.

But let’s say I did something different. I actually release two versions of alice-bob-instance. A version 0, which was a completely empty package with no dependencies. And a version 0.1 which actually has the implementation of the alice-bob instance, but not the instance declaration itself.

Now anyone needing the instance will actually need to import alice-bob-instance > 0, but that’s not a big deal.

Then, inside alice.cabal, I do the following:

Flag alice-bob-instance-flag {
  Description: Enable Bob instance
  Default: False
  Manual: False
}

if flag(alice-bob-instance-flag)
  build-depends: alice-bob-instance > 0
else
  build-depends: alice-bob-instance = 0

Then I can use CPP to conditionally import the alice-bob-instance module and also to implement the instance (which will just be a call to the code in that module).

It seems like this just avoids the orphan instances, whilst ensuring that instances (and hence dependencies) are only pulled in if they’re explicitly needed. Also whilst this could be done with manual flags, if a package that depends on alice and bob needs the instance whilst it can’t set a flag, but it can set an dependency on alice-bob-instance > 0.

Now arguably this is a little naughty as a flag is changing the external interface of the module (i.e. we’re exporting a new instance) but surely lots of automatic flags do this, as they seem to be designed to allow compatibility with a larger set of dependent library versions. Unless a library is completely opaquely wrapping the types in that dependent library something is going to leak into the interface anyway.

Is this a pattern that’s okay to use or is it a terrible idea?

I don’t think changing the external interface of a module with flags is very common, actually. It’s not a good idea. But this is a bit of a contentious area; the policies and norms are underspecified.

I was actually just looking at a discussion that showcases the disagreements. If we engage in some blame-free retrospective action (i.e., be nice to everybody in the thread), there are some generally-applicable observations made therein. The link to the thread.

Thanks for the link.

Reading it provoked quite a bit of thinking.

On reflection there’s two problems with what I’ve suggested.

  1. I’ve realised in what I’ve described actually a circular dependency between alice and alice-bob-instance, in that they both depend on each other. So I don’t think that will work.
  2. The other issue is that if a package depends directly on alice and bob but only indirectly on alice-bob-instance just a change in the dependencies of it’s dependencies suddenly can make it break (namely if one of it’s dependencies no longer depends on alice-bob-instance). That’s no good.

There’s does seem to be no better solution than the orphan package and having to explicitly import the orphan package module without an import list, but this does seem deeply unsatisfying.

That’s how you’d import plain functions, so I don’t see an issue.

Ultimately a proper solution here would be a new Cabal(/GHC?) feature, somehow giving alice the right to pull alice-bob-instance if bob is also present. This is not the kind of information you can encode into a manifest by yourself.


Also do consider that not everything needs to be an instance. They should be unambiguous, and the QuickCheck discussion above is over a type class that is, well, Arbitrary.