The golden rule of software distributions
haskellforall.com
haskellforall.com
All the packages being intercompatible sounds extremely valuable, but how is it actually done? For example, how does stackage allow package A to continue updating despite some never-updating package B requiring an old version of package A? Drop B? Don't let A change? Ignore B's declared dependency constraints? Take over development of B? Require every package to have a contactable maintainer who responds to "you gotta update B" emails or else the package gets dropped within a month?
* For an example of "Drop B", at one point the haskell-lsp package was deprecated in favor of the lsp package, so all downstream packages had to drop the haskell-lsp package as a dependency and migrate to the lsp package (I personally had to do this)
* For an example of "Don't let A change", that might happen for some period of time, although not indefinitely. The most obvious example is holding back the compiler version. For example, Stackage was on GHC 8.10 for a while, even after GHC 9.2 was released, due to breakage introduced in GHC 9.0.
* For an example of "Ignored B's declared dependency constraints", this is extremely common, especially when the `base` package is upgraded (since many packages have conservative upper bounds on their `base` dependency which can often be trivially bumped without issues).
* For an example of "Take over development of B" the `aeson` package for JSON support is one example of this. More generally, this happens when abandoned packages get adopted by the Haskell organization.
And Stackage does require contactable maintainers for supported packages. There are some exceptions to this rule, though. For example, sometimes a package gets added where the maintainer is available, but a dependency for that package was not yet on Stackage. I believe you can either add that maintainer to be the contact for that dependency, too, or it can be an orphan package. There are a bunch of people who fill in the maintenance gaps in the ecosystem by fixing these packages that don't have official contacts or active maintainers.
So just allow installing more than one version of a given package? Problem solved :)
> A locally coherent package manager requires a globally coherent software distribution.
Ah, so the implication is that "local incoherency" is bad? There are certainly some specific packages where you don't want multiple versions in use at the same time, but for most packages it's fine. For the sake of discussion, let's call those packages where having multiple versions is a problem "key" packages.
The problem with the solution in the article is that it requires some third party (ie. Stackage maintainers) to do a ton of extra curation work, plus every package that might depend on a "key" package needs to do work to make sure they're using the same version as everyone else if they want their package to be included in the next curated set.
An ideal solution would only require extra work for the maintainers of those "key" packages. This can be achieved by splitting the "key" package into two parts:
1) The "key" part of the package - only one of these can be in use, but has no public API. 2) The adapter package - depends on the "key" package and exposes a public API.
Since the adapter package is not a "key" package, it is fine to have multiple versions, and since no other packages will directly depend on the "key" package, there's no extra work for anyone else.
The maintainers of the "key" package will continue to release patches to older versions of the adapter package to ensure that all adapter versions are compatible with the latest version of the "key" package.
Now, all of my dependencies expect values of A1.X. Except for one, that upgraded recently and expects A2.X. Now I have no way at all of taking a value from one library and sending it to that new one. It's only useful it it's some stand-alone functionality that I must call directly from my code (like the vast majority of libraries in OOP, so, maybe you are biased?) and not integrate with anything.
There are two approaches to solve this that I know of:
1) When releasing UUID 2.0, also release a new UUID 1.X which adds a dependency on the new UUID 2.0 package.
Then either: a) Re-export the UUID 2.0 type from the UUID 1.X package.
b) Add conversion functions/operators to the UUID 1.X package.
This approach is nice because there is no extra overhead for the 2.0 dependency, all of the extra compatibility code only exists in the 1.X package.
2) Discourage passing around UUIDs directly. Instead have a trait/interface/etc. called "UuidLike" or similar, that is implemented for UUID-like things, and have downstream code be generic. This trait would allow converting to/from any version of the UUID type.
A similar approach as in (1) can be used to share this trait across multiple versions of the package.
For this example of a UUID, approach (1) makes more sense to me, but in practice there may be cases where approach (2) makes more sense.
There may also be cases where it is preferable to just break compatibility. An example of this would be if the UUID 1.0 package had a sufficiently serious design flaw (eg. always assumed type 1 UUIDs or something). In that case the ecosystem churn may be preferable than trying to support something inherently broken.
For example, the newly released `mtl`, which is a dependency of many other packages, breaks backwards compatibility from version 2.2 to 2.3. Personally, I'd expect to be able to have a dependency requirement like `mtl = 2.*` and forget about it forever.
If package versions were 'updated' to follow semantic versioning (e.g. `mtl` is not implicitly a version 3), we might have a better view of the current level of coherence of the Haskell ecosystem.
What is perfectly reasonable, by the ways, because it makes the schema basically useless. If people did it, very soon all the packages would be free-form 0.X versions, if not by their fault, it would be because of a dependency.
It makes much more sense to forcefully increase the version, instead of decreasing it.
(Also, notice your sibling that explains that mtl strictly followed the numbering convention.)
Thank you for bringing this to my attention!
In that terminology, Linux distributions are "mostly coherent" in their rolling beta release and the strive to become globally coherent for their release forks, right?
What do we call NixOS, where different users can have incompatible package versions but the whole system allows for that? 'Tolerance' rather than 'coherence', perhaps?
This is why (thank you NPM and NixOS) we now have a strong trend of project-relative package trees.