The ^>= operator in .cabal files

I think what @maxigit is trying to say is that instead of writing ^>= 3.2 you could just as well write >= 3.2 (without < 3.3 upper bound) because it is always the case that there is an implicit “I have no idea if this works with 3.3”. It does not make sense to interpret a lower bound (without an accompanying upper bound) in any other way (you can’t know what happens in the future).

One problem with this is that you might know it works with 3.2 and 3.3 but 3.4 does not exist yet. What should you write then? Maybe >= 3.2 && >= 3.3, but that seems redundant.

5 Likes

Oh I see - this does make sense to me. Ha.

It feels to me that this is moving in the direction of having a list of known working versions, plus at most one non-working upper bound. With that in mind, perhaps we should stop thinking in terms of intervals, and directly write out the list of working versions as something like [3.2, 3.3]? With an optional upper bound for known non-working versions: [3.2, 3.3] && <3.4. Of course we could hardly get rid of the current version bounds syntax altogether, but it might be interesting to consider what could be done to support this pattern.

2 Likes

I think you can already write (== 3.2 || == 3.3) && < 3.4, although I’m not sure about the parentheses because they aren’t documented.

But that only permits exactly 3.2 (not even 3.2.0) doesn’t it? IIRC that’s what ^>= was introduced to tackle.

But in general I think you do want version ranges, because the PVP is supposed to tell you, “If 3.2 works, then 3.2.8 will work as well.” Of course, nothing except hard real-world experience can tell you if that’s true. But if we don’t accept it as a possibility, and instead require our users to only use our specific version bounds, it would be impossible to ever build any package due to conflicts. Users would be stuck immediately ignoring all those specific bounds.

The funny thing is that if you do have hard real-world experience, version bounds are unnecessary. And if you don’t have hard real-world experience, then version bounds are at best a “possibility”. It does throw the whole delicate scheme into question. (And yes, this debate has been trod many times before. Stackage is one answer to the question. Another potential answer would be even more automation that can help packages give better guarantees about their dependency constraints.)

1 Like

Note that we already have a set notation:

^>= {3.2,3.3}
7 Likes

Yeah, and there’s also == 3.2.* if you do know that 3.3 doesn’t work.

1 Like

This one is not in the docs for build-depends!

Edit It actually is! Sorry for the false alarm.

It is, actually. :slight_smile:

I might… just… rewrite that section…

2 Likes

Oh wow, it is! Now I see it!

I think that the chronological presentation threw me off. My bad.

I like that line of thinking, and suggest to go even a step further: Don’t write the list of working versions, but gather it from your test suite. Assuming that “working version” can only mean “tested version” (because else you don’t know), you can fully generate the build-depends version from your CI tests:

This tool creates dependencies like text ^>=1.2.4.1 || ^>=2.0.1 for you. You no longer have to think about how to write your build dependencies – instead, you think about which configurations to test on on your CI.

3 Likes

Maybe it could generate set notation instead :stuck_out_tongue_closed_eyes:

Yes, easily. I think I didn’t do it because the notation doesn’t buy much when you are generating it manually anyways, instead of having to write it, and I was worried about cabal version backward compat. But maybe there is no reason to support pre-cabal-3.0 here anyways? It doesn’t matter much either way.

It’s more that I haven’t read everything from the Cabal User Guide, and hadn’t encountered this operator.
I’ve seen it pop up in some libraries on hackage, which is why it’s not completely new to me, but I just didn’t think too much about it.
“Must be up-to-the next (minor) version… or something?” was mostly my thoughts when encountering it. :sweat_smile:

Never had any trouble building things with that operator, so didn’t spend any brain cycles on it :upside_down_face:

2 Likes

Don’t worry, it was mostly a sort of joke. :slight_smile:

1 Like

So what happens when somelib-3.5 is released and it’s compatible with the dependent package? If the quoted definition is exact, Cabal is supposed to reject it by default. Should the package author then update the bounds to somelib ^>= 3.4.2 || somelib ^>= 3.5? And for the next release somelib-3.6 to somelib ^>= 3.4.2 || somelib ^>= 3.5 || somelib ^>= 3.6 etc? This doesn’t seem practical.

The soft bounds generated by somelib ^>= 3.4.2 should be read as somelib >= 3.4.2 && somelib <= the-latest-published-version. I hope that’s the actual semantics of allow-newer: ^somelib.

2 Likes

I think the best plan is to use the {} set notation for this case. It’s still pretty tough for when you actually support a very wide range of versions. In general packages should probably only be listing the versions that they actually test against, which is likely not that many.

After @david-christiansen posted this I tried out switching one of my packages to the ^>={} notation as an experiment. One thing that didn’t quite work out is that I support text all the way back to 0.2 because the only things I use from it are pack from the strict and lazy modules. It’s probably useless to claim to support that many versions, but it starts to explore the problem you’re asking about.

3 Likes

Yes, when the package author learns that the changes in the major version number didn’t break compatibility then the bounds should be augmented. The whole point of ^>= is that you’re using the package in a way that minor version changes (according to the PVP) wouldn’t break your package. But major version numbers can change anything, so you’ll need to revisit the issue and then declare that things are still OK.

FWIW One thing I’ve stated doing with my packages is removing all lower bounds, test with cabal’s --prefer-oldest flag in CI, and add back lower bounds as necessary.

Technically, all I’m doing in CI is testing all the lower bounds on the lowest version of GHC I support and all the upper bounds on every version of GHC I support, so I’m not testing the full matrix (oldest version of X dependency + latest version of GHC, etc.). But I think this is good enough coverage. And packages that actually highly depend on a dependency (e.g. persistent for my persistent-mtl library) I also have the specific versions of that dependency I support to the matrix.

2 Likes