Using different versions of the same dependency like Rust

Yeah, that sounds like it could work. We can also copy cargo’s feature of renaming packages which solves the package imports problem. Imagined syntax:

  build-deps: base
            , http-types ^>= 0.12.5 as old-http-types
            , http-types ^>= 1.0.0  as new-http-types
import "old-http-types" Network.HTTP.Types as Old
import "new-http-types" Network.HTTP.Types as New
6 Likes

That’s a really interesting idea I wasn’t aware of. :thinking: I’ll take a look at that in Cargo.

Assuming the plan is to get http-client/wai to adopt the new types, then it doesn’t sound like renaming the packages will help. In rust, this will result in a there are multiple different versions of crate http-types in the dependency graph error if the user doesn’t have the same http-types as its dependencies.

I am not a Rust user, but all the information I can find online points to Rust allowing multiple different versions of the same crate in the dependency graph (e.g., Articles - Stephen Coakley). Why wouldn’t that work in this case?

This is fine for internal libraries that are not re-exported. I assumed that a new http-types will come to end users through http-client or wai, which will requires using the same versions.

1 Like

See also private dependencies which has a working prototype afaik. It’s a similar idea but rather than renaming, you can depend on a package and then guarantee that it’s not exposed in your public API, and then you can have multiple versions in the same build graph Private dependencies · Issue #4035 · haskell/cabal · GitHub

4 Likes

I don’t think it has to be limited to private/internal libraries. The APIs will be incompatible, but one could write a compat package to map types from one version to the other, or it may just be that the particular parts of the API that are incompatible are not actually used together. Again, from all my internet searching that seems to be what Rust does (e.g., it produces errors like this: https://stackoverflow.com/questions/78298605/rust-duplicate-dependencies).

FYI Go also had a similar idea. You can depend on multiple major versions of the same library, but the there is no renaming at the library level - multiple different versions are denoted with import.path/v1, import.path/v2, etc.

1 Like

As far as I understand, a lot of these problems (and many other problems) could be solved with a proper module system, right?

module X (
  module Y as YVersion2
  )  where

import Y qualified as Y  -- Pavkage version 2 of package with module Y in scope

Or at least, a proper module system could be part of the solution.

1 Like

If you are expected to be able to use two different version of the same library in one process, then I assume that means that the contents of the two are disjoint (or the build infrastructure makes them so, i.e. makes ModuleA become ModuleA.v1 and ModuleA.v2), so it seems much simpler for these to just be packaged as two separate libraries–that’s easy to understand, and no new tooling is required.

This is qualitatively different than just a major version change–a major version change means that things can break, but this means that de facto everything has changed.

Or to put it another way, if I can use multiple version of the same library side-by-side in an application, then that really strains the notion of it being “the same library”.

2 Likes

Well yes, I think thats my point. Having this module system is even more general. You can have two completely different packages but also one package with two different versions. As long as the import distinguishes between the two, and you can also export them under different namespaces.

1 Like

The problem is that every breaking change currently requires lock-step upgrades throughout the ecosystem (See the previous thread linked at the top). Temporarily allowing two versions of the same package to be used enables more incremental upgrades.

1 Like

I’m not convinced this is a good idea:

  • can easily blow up binary size… now I have multiple versions of conduit and amazonka in my depgraph?
  • makes it harder for the end user to constrain dependencies… I suspect we need to add syntax to cabal.project (constrain a dependency for the whole depgraph or only for a single package)
  • probably makes solver errors even more peculiar
  • mixing two versions of a package is a potential footgun

We’re trying to solve a social issue with a technical solution again is what I feel. Library maintainers should strive for portability (support multiple major versions of a dependency), but they also need to know which support window makes sense and what versions are actually supported upstream.

All of this requires some form of communication, either automated or coordinated.

Sadly, hackage is not really a collaboration platform.

8 Likes

I agree a better module system could be an alternative, but I don’t quite understand your code example. Can you explain it in a bit more detail?

Not to climb on to a dead horse, but doesn’t backpack/mixins already make this possible? Or does it get disallowed because they’re the same underlying package…

[Edit] seems it is possible: [RFC] "http-types" breakage / additions / rework - #51 by fendor

I’m not sure what PR @fendor is referring to. The private dependencies issue mentions mixins, but not how to use them to use the modules of two different versions of a dependency.

I am referring to this cabal PR feature: Private Dependencies by alt-romes · Pull Request #9743 · haskell/cabal · GitHub

The solution is mixins, but with new scopes that allows to solve for private dependencies separately.

1 Like

I think we should think whether this is a good idea. Rust’s ability to import several versions of a single package provides an easy baseline. But also has problems. Like, multiple versions of a single package can in turn depend on multiple version of sub dependencies, which can result in an explosion of dependencies size

Something i have been exploring in idris2, is making new library versions backwards compatible with old ones. In idris2 you can make a multiple functions with the same name. If a single function needs to be updated to have more or different arguments , you can make a new one with the right ones, then deprecate the old one, remove it later down the line
There are similar options for data types and whatnot

Haskell does not have function overloading, but it can be emulated by defining a typeclass to emulate a function. When needing to update the arguments of a function , you could instead define a typeclass to emulate an overloaded version of the function. Several versions later, after your library has removed the old function, the new one could be converted to a normal function . There are even options to smooth over this transition. Extreme edge cases would care whether something is a function or typeclass emulating a function, but this could be dealt with

1 Like

the killer feature is if we could allow modules that haven’t changes between versions to be confluent

eh maybe we gotta go further and adopt protobuf esque rules around records

overall maybe we ban this and haskell companies focus on enabling their teams to depend on different versions of things and upgrade at their own pace

just cuz i want foldMapA doesn’t mean i should have to rebuild every time someone changes the internal utils package.

Since we’re brainstorming already…

What if we took the existing *-compat semi-solution to the backward compatibility problem and made it an pervasive shim? Suppose there’s a dependency graph {A -> {B, C}, {B -> D.1}, {C -> D.2}} where D.1 and D.2 are two versions of D, and there’s a D-1-shim library that can act as a full replacement for D.1 while depending on D.2. Already today, we can use cabal.project to redirect all D.1 dependencies to D-1-shim (not sure if possible to redirect to Hackage or only to its Git repo?). What’s missing is making this automatic by registering D-1-shim as a pervasive redirect, instead of every user having to discover the solution individually.