Immutable Publishing Policy
This is a policy for publishing Haskell packages. It was published in February of 2022. The aim of this policy is to remove all breaking changes to downstream users of packages, where feasible. It is most applicable to packages which have users, and/or have a more or less stable API. It prioritizes downstream users at the expense of author convenience.
It has been implemented in the lucid package.
By “levels” in this description, we mean “module” is the first level, and “package” is the level above.
- If you require more, that’s a break.
- E.g. adding an extra field to a record or requiring a class constraint, add an extra constructor which users can pattern match on, or another package dependency or newer major version of a package that wasn’t needed before.
- If you provide less, that’s a break.
- E.g. if you remove an export or a field of a record, return less or different from a function, remove a module, or an instance of an exposed type.
- If you change something, that’s a break of the next level up.
- E.g. if you change the type signature of an exposed function, or change a data type’s definition which is exposed.
- If you just add something, then just bump the minor version (which is the only thing that changes in this scheme) of the package; you can use the current date e.g.
- E.g. a non-orphan instance of a type from your own package.
- If you add an orphan instance in your library–which you should not do–that’s definitely a breaking change for the whole package, bump the package.
- A user may have written an orphan instance for one of your types, but this is a departure from standard practice, and therefore isn’t protected from breaking change. You should feel free to add new instances for your own types.
- Be careful about leaking instances. If you bump your package when you bump your dependencies major versions, then you should be safe from this.
This differs substantially to the PVP. ⚠️
- If you want to change a thing
foo, just make a copy of it and call it
- If it’s impractical to add another version of a thing (because, e.g. you changed a class or a data type that’s used in many places), then copy the whole module
- If it’s impractical to just copy a module, then it’s time to copy the package,
- If you want to rename or remove something from a module, clone the module. If you want to remove or rename a module, clone the package.
- It’s more effort for an author to make breaking changes. The author pays a price, and has to think twice about whether it’s worth it.
- The user optionally pays a small price to upgrade to new functionality, rather than a mandatory tax for changes they don’t want, which is the PVP system.
- For more elaboration, watch Rich Hickey’s video Spec-ulation.
- Related mantra of the Linux kernel: don’t break user-space
- I don’t agree at all with this post by ezyang
- You can publish additions to your old versions of modules and packages.
- You can choose to stop maintaining them at some point. For example, a duplicate module hierarchy. You can just drop the old one and publish a new package.
- By the PVP, packages that use your package will force their users to use your newer API, which will make them unhappy. By following the IPP, you give your users the chance to import both library versions and maybe write some migrations between the two versions.
- Hackage supports deprecating a package in favour of something else. That’s your means to tell users of newer versions.
- GHC lets you mark declarations and modules as deprecated, which is issued as a warning.
- Users can use
module Lib (module Lib2) where import Lib2 to try newer versions of module hierarchies as “drop-ins”.
- For a package following the IPP, you can just depend on
package >= 0.0.n where
n is the feature that you need. You never need an upper bound because there are no breaking changes.
- Both Hackage and Stackage will allow you to upload package1, package2, etc. and so your users can continue to use the package1 if they’re not interested in package2, and if they’re not forced by PVP-following maintainers of other packages that they’re using.
Compatibility with the PVP
You can do both, the IPP is a subset of behavior within the PVP (but upper bounds aren’t required or needed by the IPP). In practice, people following just the PVP and not the IPP will be a pain for you as Haskellers change things and cause breakage all the time. But don’t bother people about it. Just do your best.
When to use this policy
I propose that you should:
- Put new experimental packages on GitHub/GitLab/sourcehut, etc. and using Stack or Cabal you can specify dependencies on that package by a commit hash. At this stage, authors can change as much as they want and users can pin to commits.
- If your design becomes stable and you want to support the package, put it on Stackage or Hackage. Now apply IPP.
For packages already published, you can apply IPP immediately.
Do you just want Unison?
Sure, but that’s a whole new language. Haskell is our language of choice, so let’s do our best to make it nicer to use.
It’s not worth doing because we should have machines automate dependencies based on fine grained uses
That’s true. Here in the real world, however, no such automation exists.
Security fixes can be breaking changes, we should make bad code go away
You don’t know how and in what setting your users are using your libraries. It’s not your place to bully them into changing their programs because you know better.
Shouldn’t old code be garbage collected?
Yes, I covered deprecation above. I also covered removing old code and the ending of maintenance above.
What if I add a constructor to a type? I’d have to duplicate my whole API!
Yes. If you add a new constructor then you’ll indeed require more of the consumer of your package to form a complete pattern match. Duplicate the type. You changed a namespace.
I’ll repeat myself: the IPP is about valuing the user’s time over the maintainer’s time. It’s not all about you and your needs. Think about others who have to consume your work.
Why have package versions when you are going to duplicate an entire module or package when you make a breaking change?
I already covered this above, but let’s rewrite in a different way: Package versions in Haskell are broken. They force all package maintainers to move in lock step. You can only practically use one version of a package in your installed package scope.
If you want to keep using foo-1.2.3 and another package you’re using wants foo-2.0.0, you shouldn’t be forced to change all your code. You should be given the option to migrate between them.