I was wondering if someone could explain something to me. I understand the issues involving inconsistent environments, but bad constraints don't really seem like a particularly challenging issue to me.
If you have libraries A and B which each require mutually exclusive versions of library C, what is stopping the compiler from adding both versions of library C to the compiled code for A and B to call separately?
Obviously this would result in larger binaries and should generate a build/install warning, but it seems like this would solve the overwhelming majority of Cabal's dependency resolution issues.
There's clearly something I'm missing since this has been an outstanding issue in Cabal for quite some time. Would someone with a better understanding of GHC/Cabal's compilation/linking/installation steps mind shedding some light on this?
I maintain the package manager for the Dart language. This question comes up all of the time. The problem is that these libraries may interact at runtime:
* A gets a value from its version of C.
* It passes that to your app.
* Your app gives it to B.
* B gives it to its incompatible version of C.
Haskell may handle this scenario differently because of its type system, but in Dart, this would cause lots of user problems.
That's exactly what can happen. Cabal endeavors to prevent multiple versions of a package from being installed simultaneously, but as this post mentioned Cabal does not maintain global consistency so if you do multiple independent installs in the same database then you can wind up with this problem.
In particular this is, for whatever reason, a frequently occurring problem with the `bytestring` library. There are a lot of SO questions related to figuring out why the compiler is rejecting the use of a ByteString type even though it appears it should work... but that's because the compiler error is eliding the package version difference.
This shouldn't really be a problem in a statically typed language. If A is using C, but all the uses are hidden, then the type system guarantees that the values from C can never be inspected by anyone outside of A. So B can use its own, different version of C.
On the other hand, if C is present in A's interface, then it s version has to agree with all other versions of C visible in the same unit of code (package/module/namespace).
That fails in the presence of subtyping, unfortunately. A could expose a C in terms of some supertype not defined in C. Then that object gets cast back down to the more specific type and handed off to (the wrong version of) C.
Well, in that case, the same class from different versions of C should be considered distinct (i.e. when the compiler sees `a instanceof C.A`, it actually translates it into `a instanceof C_V1_53.A`, where the version is appropriate to the current module (and so is different if accessing `C.A` in modules `A`, `B`, or potentially the main program).
Hmm, npm with Node.js allows that to happen. I don't recall that causing any problems, though. The "solution" seems to be that B is tested with both A and C, and when you're developing B, you know what kind of values you get from A, and you know what kind of values C accepts.
It doesn't cause problems most of the time since most dependencies do tend to be encapsulated. When they aren't, JavaScript's dynamically-typed nature can also give you some slippage: you may get an object from a different version of yourself, but as long as it has the same properties you expect, it might, mostly, do the right thing.
This is also a better fit in JS where the community seems to prefer packages that don't expose many real "objects" with methods and stuff. Packages tend to expose bare data-bags and functions from what I've seen. That makes "I got an object without the methods I expect" errors less frequent.
I personally feel that approach is too unsafe and definitely wouldn't have been a good fit for Dart, but it does kind of sort of work for npm.
Of course, it's totally broken in the presence of dependencies cycles, which is why npm now has "peer dependencies" and is right back to having to resolve shared constraints.
You probably would have good vocabulary for this in the ML family languages. You're essentially talking about existential type mismatching—a core component of how modules work in ML family languages.
It's just hard to read that into Haskell since "modules" are not as well represented in-language and exist somewhere between Haskell-the-language and Cabal-the-package-manager/ecosystem.
I think that would only work if neither A nor B exposes anything about C. I'm not a Haskell user, but certainly in Scala and Ruby there are plenty of libraries where library consumers see objects from underlying libraries.
Suppose C is a JSON library, for example. In my code I fetch a JSON object from A, with library version C1. What happens when I pass that object to library B, which uses C2? Is that an immediate type violation because C1 and C2 are treated as different types? Do you live with runtime failures when, say, B calls a C2 method not in C1? Do you you try to create some sort of magic object conversion and hope that the data isn't too different? Or do you try to require all libraries to describe how to convert data from every version to every other version?
Even if the two libraries never interacted and data could be proven never to flow between them, there's a real question as to what version of the library is available from the main code.
Despite my concerns, I suspect that you could make this mostly work if you were cavalier about possible runtime errors, because actual conflicts would be rare. But from what I understand of the Haskell community, they're not big on "cross our fingers and hope it works at runtime" approaches.
If you have libraries A and B which each require mutually exclusive versions of library C, what is stopping the compiler from adding both versions of library C to the compiled code for A and B to call separately?
Obviously this would result in larger binaries and should generate a build/install warning, but it seems like this would solve the overwhelming majority of Cabal's dependency resolution issues.
There's clearly something I'm missing since this has been an outstanding issue in Cabal for quite some time. Would someone with a better understanding of GHC/Cabal's compilation/linking/installation steps mind shedding some light on this?