Roman Cheplyaka

Why PVP doesn't work

October 5, 2013

The Package versioning policy is one of the controversial topics in the Haskell community. Specifically, the point of disagreement is the upper bounds on dependency versions. Some people consider them necessary, while others consider them harmful.

To remind the arguments of both parties, here’s a typical conversation between a proponent and an opponent of upper bounds:

pro-pvp: A major version change indicates incompatible changes in the API. Your package could fail to compile against that new version!

against-pvp: Well, it could fail, but most of the time it doesn’t, because it uses only 10% of the package’s functionality, and the incompatible change affects some other 5% of the API.

pro-pvp: Why take chances? If you pin down the versions with which you have tested your package, you know it will always compile, no matter which new versions will be released in the future.

I belong to the opponents of upper bounds. Instead, I simply make sure my packages work with the latest everything. This is similar to the Stackage philosophy.

I said «simply», while in practice this can be somewhat time-consuming, given the pace at which the language and libraries evolve. People have complained in the past that maintaining a Haskell package is like shooting a moving target.

But that’s the only way it’s going to work. Below are two reasons why the alternative — enforcing upper version bounds — doesn’t work.

Reason 1 — incompatible constraints

Consider this situation:

  • package-a depends on containers ==0.4.*
  • package-b depends on containers ==0.5.*

It’s great that I can compile both of these packages, but what’s the point if I cannot use them together in my application?

Interval constraints do not compose — the intersection of non-empty intervals may be empty. Having just the lower bounds is much better — if each one of them is satisfiable, then they all can be satisfied simultaneously (e.g. by the latest version).

Reason 2 — you can’t nail everything down

In theory, you pin down every dependency and in five years from now it’s going to build in the same way as it just did today.

In practice, people would want to build your package with modern compilers, on modern operating systems, and with external libraries provided by their distributions. These external changes, which you cannot control, will likely cause some of your dependencies to stop building in the future.

To give you a specific example, at some point GHC became more strict (and rightly so) about type synonym instance declarations.

This code

{-# LANGUAGE TypeSynonymInstances #-}

class C a

instance C String

compiles with GHC 7.0, but GHC 7.2 requires also to enable FlexibleInstances, because it’s needed when you expand the synonym.

Every maintained package which has this problem would sooner or later get the fix, but probably only the latest major version would be fixed. If you depend on some older version of a problematic package, you won’t be able to build on anything newer than GHC 7.0.

Conclusion

My first argument shows that putting upper dependency bounds can create obstacles to using your packages, and the second argument shows that upper bounds in practice don’t deliver on their promise of eternal buildability.

So, dear package maintainers, please don’t put the upper bounds on your dependencies without a specific need.

Update

Thanks for everyone who responded. You helped me understand the problem better.

Turns out there are two separate issues here:

  1. Whether to use upper bounds, and
  2. Whether to support the latest versions of your dependencies.

They are related in the sense that in order to avoid supporting the latest versions of your dependencies you have to use upper bounds to keep your library compilable. However, simply using upper bounds does not mean that you do not support the latest versions.

So, my request to the library developers is: please support the latest versions of your dependencies. Failing to do so (and relying on upper bounds to have the illustion that it’s fine) is exactly what causes the problem with incompatible constraints.

As soon as you support the latest versions of your dependencies, upper bounds become harmless but also useless.

They may give you a grace period when a new version of a dependency comes out, but if you stretch this period, then you’re breaking your responsibility to support the latest versions.

On the other hand, having upper bounds puts more strain on you to keep those bounds up-to-date. Plus, not having upper bounds makes it more obvious when you don’t actually support the latest versions.

But at the end, if you are diligent enough, it’s up to you whether to use upper bounds. My personal choice is still not to put them.

Also note that this doesn’t apply to application developers (at least when the application doesn’t support dynamically loaded plugins). In fact, application developers can compensate for the absence of upper bounds in their dependencies by putting the constraints in their own cabal files.