I feel like at its core the issue here is the coordination problem laid out in the GHC.X.hackage proposal in that it’s extra work for maintainers to coordinate upgrades of their own dependencies. This is especially true if you just want to accept a PR, publish a release/revision and then not have to worry for the next 6 months.
Similarly for end-users it can be a lot of extra work to follow N PRs/releases and remove your extra deps when appropriate and folks will often want to just wait for a Stackage snapshot where everything is already working well together
We can’t force anyone, but it’s ok to be judgemental.
And I’m definitely displeased seeing that it’s getting harder to pick a proper recommended GHC version in GHCup when parts of the ecosystem drop support for everything that’s older than 3 releases.
It’s just a sign that the cadence of GHC releases is unsustainable both for GHC team and package maintainers. If we had one major release per year, there would be much more time and resources for GHC team to stabilise them, so that ghcup can mark them as recommended. And for maintainers supporting three latest releases would be equivalent of supporting for three years, which is long enough for updates to reach even deepest corners of the ecosystem.
I’m seriously considering to sit out GHC 9.14 if the situation does not change, I had enough of this treadmill.
If we want to have a modern, evolving language then we need to mark when we have breaking changes, and major version changes are how we do that. But often those breaking changes won’t affect most people and packages.
In these situations it makes more sense to release these breaking changes when necessary and hope that people are sensible about what versions their packages support.
The Aeson 1 to Aeson 2 transition was special (or at least I hope it won’t be replicated). There is no way (provided by the aeson package itself at least) to write code that is compatible with v1 and v2 at the same time. That was a completely unforced error, and responsible for pretty much all of the pain of that transition.
To put it into some perspective, both @taylorfausak and me maintain 40+ packages. When a new GHC (major) version arrives, even a brief review takes, let’s say, an hour per package (assuming that breaking changes were reasonably minor). This amounts to a full working week, and this is not really anything exciting to do. Do I want to waste a week of unpaid drudgery every half a year? I’d rather not. Doing it once a year would be much more manageable.
Can you elaborate on the benefit to you, with regard to this issue specifically, of GHC releasing once every year rather twice every year, versus you just deciding to only bump your packages once every year, skipping every other GHC release?
That is a lot of work and a lot of packages, for which I thank you both for maintaining and providing in the first place.
Even 40 hours once a year is a lot from my perspective! If they are so much work, have you considered trying to share the load? Maybe getting a small group of people that are relatively competent to act as helpful caretakers so instead of it taking you 40 hours and however many evenings to get through it, it takes much less time.
I don’t have a clear overview of what exactly causes those breakages, but from my understanding, most of them are actually related to changes in base or core libraries? Less so regarding surface language or changes in extensions?
That means that we could, in theory, have slower cadence regarding base and core library updates in GHC. That will put some burden on core library maintainers to maintain more branches, potentially, but that’s what PVP was ultimately about.
Seconded! It’s a huge amount of work and I had no idea that was the amount of work involved for @Bodigrim to maintain his packages (and @taylorfausak too), so thanks very much to both of them (and to anyone else doing similar amounts of work). It seems like finding ways to boost the efficiency of this process should be high priority for the community.
Consider one of the most extreme examples of breakage, removal of a function: suppose foo-1 contains function barOld. foo-2 will remove it and replace it with barNew. This is what I think should happen: foo-1.1addsbarNew (alongside barNew). Then users can write code that works with bothfoo-1.1anfoo-2 by restricting themselves to barNew.
Regarding Aeson specifically, Aeson 2 introduced Key and KeyMap where Aeson 1 had Text and HashMap Text. A smoother upgrade path could have been provided by defining type Key = Text and type KeyMap = HashMap Text, and adding functions like lookup :: Key -> KeyMap v -> Maybe v that are required in Aeson 2. Then it would have been possible to write code what works for Aeson 1 and Aeson 2 at the same time. As it happened, pretty much every Aeson codebase did a “big bang” update from 1 to 2.
I don’t see the need of keeping all those mini release alive. In the present case, instead of keeping 9.8, 9.10 and 9.12 alive should maybe keep 9.12 (release candidate), 9.10 last official one an 8.10 (LTS).
I’d be surprised if much is gained by dropping some in-between GHC versions.
In my experience, work is caused by the oldest and the newest GHC versions I want to support.
The thing is that my estimate above (1 hour per package per GHC major release) is foremost an administrative cost, which is to be paid even if there was no breakage at all. Here is a typical workflow:
git clone a package.
Build it with a new GHC, figure out any allow-newer and source-repository-package necessary.
Test with a new GHC, especially doctest. Fix any discrepancies, hopefully minor.
Benchmark with a new GHC, compare against old GHC.
Fix any new GHC warnings.
Fix any cabal check warnings.
Bump dependency versions.
Update changelog.
Regenerate CI.
Push and wait for CI to complete. Review results.
Raise pull request, request reviews.
Next, cabal get the released version of the package.
Bump dependencies, compile, test and benchmark it.
Decide whether a Hackage revision is enough or do we need to go for a new release.
I’ll spare you of the release checklist.
None of the steps are particularly difficult or time consuming on their own. But it’s a lot of small actions and multiple context switches.
The checklist above is for major releases, sorry for confusion.
Or better don’t even waste time on intermediate releases. Anyone interested to follow bleeding edge developments can build GHC from sources.
I mean, we are still waiting for the final release in GHC 9.6 series. There was no 9.10.2 yet. There is no release in GHC 9.12 series without critical correctness issues (#25784). Yet GHC 9.14 is expected to be forked next month. Who exactly benefits from such cadence?
Let’s assume it’s just a major GHC version, but all the core libraries, including base only have minor bumps… what do you need to do?
My guess is nothing.
On the other hand, if there are major bumps in core libraries, but nothing else, we could definitely automate part of that process on hackage:
relax upper bounds to match the shipped versions of the new GHC and build the package in CI
on success, create a draft hackage revision that the maintainer can just accept with a click
This is technically possible, although there are a lot of details to consider.
So now my question is… could CLC simply demand that there is only one major base bump per year? GHC can still pump out two major releases, but the end user experience would be different.
It doesn’t much. Since 9.8 there have been few incompatible changes in GHC, base and boot libraries. In fact in 9.12 almost nothing has been reported (although I suspect 9.12 is little-used).
Thanks to the work of the HF Stability Working Group for this.
Well, me for one. I’m looking forward to getting my hands on what will be in 9.14! Of course, I could just build my own, or, more simple for me, use a nightly build. Whether the existing cadence is better overall for the ecosystem, well, that’s a good question. I don’t know.
I like this idea (but indeed there are lots of details to consider, and lots of work to be done).
This seems plausible.
My view continues to me: we should very strongly discourage breaking changes, more strongly for things deeper in the dependency tree, so GHC and base should be discouraged from breaking the most strongly.
You are much more diligent than me! I think I save time compared to you by being more lax. My workflow is more like
git clone a package. Change to the package directory on my system, because I already have them al cloned. git fetch to ensure I’m up to date with master
Build it with a new GHC, figure out any allow-newerand source-repository-package necessary. I generally don’t bother using allow-newer because I don’t feel the need to update my package if my dependencies are not yet updated, but sometimes it’s worth getting ahead of the game. I almost never go as far as source-repository-package.
Test with a new GHC, especially doctest. Fix any discrepancies, hopefully minor.
Benchmark with a new GHC, compare against old GHC. I don’t bother benchmarking. I can get away with this because I’m not maintain critical, performance-sensitive packages like text and bytestring
Fix any new GHC warnings. I don’t bother fixing warnings
Fix any cabal check warnings. I don’t bother fixing warnings
Bump dependency versions.
Update changelog. I generally don’t bother updating the changelog if all I did was
Regenerate CI. I don’t regenerate CI, I just add the new compiler to GitHub Action’s ci.yaml that uses haskell-actions. (I’m not sure what CI setup requires regeneration.)
Push and wait for CI to complete. Review results.
Raise pull request, request reviews. I typically maintain packages where I can unilaterally update to the latest GHC release without anyone else’s authorization.
Next, cabal get the released version of the package. I didn’t understand this part. Doesn’t it imply a Hackage release has already been made?
Bump dependencies, compile, test and benchmark it. Seems like this has been done twice now, locally and in CI, so I wouldn’t bother doing this a third time.
Decide whether a Hackage revision is enough or do we need to go for a new release.
So I suspect my life is easier than @Bodigrim’s in this regard because I maintain fewer critical performance-sensitive libraries. So thanks very much to @Bodigrim for doing this (and to everyone else who does so too).
This conversation makes me think that we need to do a better job of catching performance regressions in fundamental libraries like text and bytestring before a GHC is released. Perhaps we could teach head.hackage to run and compare benchmark results.
Just commenting on this particular technical aspect, since about 2017 it’s been really trivial to add a so-called extra-dep to your stack.yaml file which essentially exists to use a newer or modified version of any given package and stack is happy with that. This is either a different version or a git repository at a given commit. To support exactly this workflow which happens a lot at companies where you need a new version of a library that fixes some kind of bug or has a new feature that would really benefit your project.
Finally, there is some kind of quarterly stack snapshot bump and then you are usually but not always able to delete some of those lines.
This is a natively supported built-in feature of stack and takes one line, in Nix it’s slightly more involved or not supported, but every company has a slightly different Nix configuration, and set of of tools. It’s hard to speak for all cases.