How much effort does backwards compatibility require from library authors?

It’s not about changing lower bounds, but about expectations of others if lower bounds stay with very old unsupported versions of GHC.

This is same in any sw development. You need actively set lowest supported version if you don’t want be overwhelmed with support requests/bug reports for things you don’t have power and time to support.

Of course things can change when are money included and many OSS companies live from supporting really old codebases which upstream haven’t power and time to support.

Here’s the software you might want to use for that: GitHub - IPDSnelting/velcom: Continuous benchmarking

Example instance VelCom (although this one is barely functional because the instance is so large)

2 Likes

My experience is that most of the time it’s not about base, but about template-haskell, ghc-the-library, Cabal-the-library and such.

Could you name anything specific?

I find it easier to persevere and do it all at once. The alternative of “if dependencies do not compile, make a note in your calendar to check it next week” means way too many context switches for me.

No release has been done so far: we just brought Git HEAD in line with the latest GHC. Now it’s time to assess whether we need a new release or the latest Hackage upload (which could be far behind Git HEAD) still compiles.

That was attempted in !271: Run benchmarks for bytestring/text/containers packages · Merge requests · Glasgow Haskell Compiler / head.hackage · GitLab, it would be great to land it.

5 Likes

The things I’m interested in are various elements of dependent Haskell, which I use more and more at work. With the roadmap as reference, I am particularly interested in the approved features syntactic unification and unsaturated type families (though I don’t know if they’ll appear in 9.14 specifically). I’m also particularly interested in existential quantification, constrained type families, type level lambdas, dependent products and dependent sums, although I suppose those features are some way off.

(Thanks to @int-index and @sand-witch for their work on these features, and to Serokell for providing the opportunity.)

Do you mean, then, that you use source-repository-package to apply needed updates to dependencies that don’t compile with allow-newer? Then what if your changes are not accepted upstream? That’s why I prefer to wait. There’s no point me going to effort to make my package compile with the latest GHC if one of its dependencies doesn’t compile anyway.

Oh, interesting. You get your Git HEAD up to date with the latest GHC before getting the most recent Hackage release up to date? Why is that? More people depend on the most recent Hackage release, after all, so it seems like the quick win arises from checking whether that compiles and doing bounds bumps if so.

In any case, I’m curious about the role cabal get plays. I’ve never used cabal get. The way I check is to do, e.g., cabal -w ghc-9.12 -b opaleye-w.x.y.z repl, maybe with some flavour of --allow-newer.

1 Like

With the continued caveat of “I don’t use Stack so I’m not talking from personal experience and may have got a completely wrong impression”, here is a recent example of the kind of thing I see that led me to believe that Stack/Stackage has trouble upgrading dependencies individually:

The problem in that thread is not that individual packages are hard to upgrade, it is about finding the right snapshot to use. Of course you could always manually specify all your dependencies, disregarding the snapshot completely, but that is a lot of work which could easily be avoided by just choosing a better snapshot.

Right, but this is exactly what I don’t get about snapshots, and exactly what seems to cause difficulty for Stack users (again, caveat: I’m only observing from the outside). I don’t see how it’s particularly useful to have a rigid set of one single version of each package. Surely it would be much more useful for a “snapshot” to be a version range for each package, each of which is known to work with all other packages in their respective ranges. (I appreciate that’s exponentially more work to verify).

1 Like

To expand on jaror’s response, that thread wasn’t just about choosing the right snapshot, but about learning the basics of Stackage and recovering from a naive accidental use of a rather special-purpose sort of snapshot (which, from a no-blame perspective, suggests the ghc-* snapshots should somehow have bigger warnings around them).

The original and enduring raison d’etre of Stackage is not just that it’s exponentially hard to define a snapshot as a set of version bounds. It’s that there’s still nobody doing it. So theoretically, it’s great. And I’m not throwing version bounds under the bus: without them, it would be impossible to discover and define new snapshots. But the big benefit of Stackage is that it exists.

In my opinion, Stackage is actually the best starting point towards providing more sophisticated snapshots. It solves the base case, finding at least one member of the class. There’s lots of directions to head from there. More platforms? Longer support for older ghcs? Matrix Haskell 2.0?

1 Like

Fair enough. I guess I just get a mistaken view from the outside. I wanted to share an example of how I get the view, but I’ll try to learn more about Stack before commenting again :slight_smile:

I appreciate hearing the outside view. Maybe relatedly, I’ve been thinking I want to start blogging, and I thought a good ironic name for the blog could be “More Wrong” – since any time I write down anything on the internet, I immediately feel the pain of exposing my own biases.

In this case, I don’t think I even explained why Stackage is useful at all. Even if that’s what I thought I was doing. I remember from the early days of Stackage (as a neutral observer) that Stackage was often unfairly compared to systems that simply did not exist, and I think I was responding to an echo of that dialectic.

To give some context for my support for Stackage: I live in an environment where Nix is obviously the necessary choice, as painful as it is. Stack is a simple and robust piece of equipment in these swampy lands. It’s a means of anchoring software components to a very specific location, so that you can find your way to them again in dark times of need.

Lots of people live in other places, and that’s totally fine. I think there’s room for many approaches. And I think increased adoption in one approach gives synergy bonuses to the others.

4 Likes

Pardon my blunt take, this is certainly off topic, but I like to think of Stackage snapshots as a “base layer”, a solid starting point — which one can extend as needed, relatively smoothly adding stuff “on top of” to cater to various needs, via extra-deps.

Analogies-wise, it’s like that FROM: line in Dockerfiles. Like the main repository in Linux distros (with all the Gentoo overlays, Ubuntu PPAs, Arch AURs to “layer on top”). One easily jumps onto a “blessed” base (less barren than the GHC base), and further builds on top of it.

“Batteries included” has its own drawbacks, and has its own merits.

A Stackage LTS snapshot is not exactly a single-shape monolith. Granted, it is somewhat rigid; for example, it’s not super easy to “just swap out” aeson for a newer version; better don’t touch text version at all. This rigidity may not be, or may be, what one wants. Nevertheless, stack trivially accommodates:

  • packages from hackage not within the selected snapshot,
  • vendored (patched) packages,
  • private repository sources,
  • temporary local forks.

This provides just enough flexibility, and simultaneously just enough rigidity, to be convenient in many cases. It’s a decent balance.

Having said that, I also saw developers fumble with just the three notions of package, version and dependency. Adding snapshot into the mix does make matters thicker to explain.

4 Likes

Because I have been asked to comment: The necessary and sufficient condition for a package to be easy to maintain in nixpkgs is that it builds with Stackage LTS, Stackage nightly and current hackage. Correspondingly the oldest relevant GHC is the one of Stackage LTS.

My personal opinion is that we as a community should strive to make updates as painless as possible instead of having wide backwards compatibility windows.

5 Likes

How?

Backwards compatible updates are the only ones that are painless.

1 Like

BTW my stack-all tool makes it fairly easy to test building one’s package(s) across multiple Stackage LTS major versions (provided you have setup the latest minor ghc versions for stack).

4 Likes

Well, I am not sure if this is a good mental model, but I think we might be mixing two kinds of “backwards compatible”. Every library is provider and consumer of APIs. As an API provider you are most easily backwards compatible by having no breaking changes. I would maybe not call this backwards compatibility but rather stability. As a consumer of APIs you are backwards compatible by being compatible with older versions of the upstream APIs you use.

Lets consider a library A which depends on libraries B and C. B and C have regular releases and lets say B depends on C. Lets consider the two extremes:

  1. Every version of B is compatible with all (older) versions of C.
  2. B is only ever compatible with the most recent version of C.

If A wants to use the newest version of B but is currently using an old version of C in case 1. that’s no problem, because B is backwards compatible we can just update B without updating C. In case 2. we have to hope that it is easy to update C so that we can update to a new C without pain so that we can profit of the newest B.

If the goal is for A to use the newest B there are two options either B is backwards compatible or it is easy to update to the newest C.

If C only ever adds cool new functions and B always immediately jumps to use these cool new functions, then that shouldn’t be much of a problem because even though B frequently drops backwards compatibility it is easy to update them both.

1 Like

The case you’re describing basically boils down to: both B and C don’t do major PVP bumps. If I’m not mistaken.

I don’t think that’s exactly the point. B could in theory make a major pvp version bump in every release in that story. But yes stability of C makes backwards compatibility of B less important/unnecessary. If we lived in a world where everyone could immediately just jump to every fresh GHC release there wouldn’t be any need to worry about backwards compatibility. Everyone could use all the shiny new features immediately, no one would need to juggle with CPP. I know this is partially a naive utopia but I think we should strive to solve the backwards compatibility problem this way. One could say backwards compatibility is fighting a symptom.

1 Like

I’m a bit at loss about your point.

If B or C make any major PVP bump, then there is no easy update by definition.

There are only easy updates if there are no breaking changes (or if libraries maintain multiple branches indefinitely, which no one does). So if B and C release new minor versions and B gets tighter constraints on C but still within the same major version… then it’s more or less invisible to A if they have ^>= bounds.

This is the same with GHC. The only way for a new GHC to be an easy update is if there are no breaking changes or if there is a way to opt out of breaking changes (e.g. language editions and decoupled base).

2 Likes

If B or C make any major PVP bump, then there is no easy update by definition.

Here is a concrete counter-example.
Suppose a library defines an options type:

data Options = Options { fooA :: Int, fooB :: Int}
defaultOptions = Options 0 0

and consumers never use the constructor directly, but instead use defaultOptions and then override fields with record syntax. Then, if we add a new field, that’s a PVP major bump but (good) consumers don’t need to upgrade their code at all, except by bumping version bounds.

We could say that version bumping is itself not easy. But I think the point is that we could put effort into improving the tooling around these things to make them much easier. The labour involved will never be zero but it could be much lower than it is now.

The way I see it these are two complimentary concerns. As an ecosystem, we want to keep breaking changes to a minimum, since that reduces the amount of maintenance labour required. But we also want to take steps to improve our tooling/idioms so that when breaking changes do happen the effort required to respond is minimal.

Side note: I know of no “definition” of “easy update” so this is not much of an argument to me.

Nevertheless you are right with everything you say and that was exactly my point. If updates are easy (and the primary way to achieve that is to have no major pvp bump) tight coupling between B and C and therefor a lack of backward compatibility in B is no problem.

My unimportant side point was this: If I really want the newest version of B it does not matter to me if B has large breaking changes, I might be willing to just bite the bullet to use the newer better API. If however B is tightly coupled to C, C has breaking changes and my other dependency D is not compatible with newest C then I am suddenly in a world of pain where I need to write patches for dependencies and the lack of backwards compatibility of B bites me.

1 Like