How do Hex and Elixir handle multiple versions of the same dependency?

Coming from a Java and Node.js background, I am really interested in how Hex/Elixir handles multiple versions of the same dependency in comparison to Maven and npm. However, it seems that this is not explained in any docs about Elixir or Hex themselves. Could someone please elaborate on this?

1 Like

It doesn’t. You can’t have multiple versions of the same application running at the same time (ignoring some caveats related to hot code loading). It’d be total insanity with named processes and so on.

Does that apply to “libraries” as well? (Sorry if some of the terminology from the Elixir/Erlang ecosystem is not so clear in mind yet, but I believe that you are referring to something else with “application.”) Suppose I want to use library “moment” to handle date/time in Elixir but I also want to use library “anotherlib”, which also uses library “moment”. Is that not possible at all?

1 Like

It will be fine to do something like that if the version constraints line up within the respective projects mix files.

For example, if Project A and Project B both relied on Project C, and the version of C specified in A was ~> 1.3 and in project B it was ~> 1.4, that would be fine. however, if the version of C specified in a was ~> 2.0 and in project B it was still ~> 1.4, you would get an error when trying to fetch the versions stating that you could not find a version of Project C that matched the requirements of both Project A and Project B.

A single module can only exist in one version on the VM. So this gives restrictions on the application structure. This basically means everybody needs to agree on a single version.

To clarify the naming: every mix project is an OTP application and that’s usually what the “application” means in Elixir.

3 Likes

Libraries and applications in the erlang world are one and the same. The notion of an application is used because while libraries are often thought of as just bundles of code, libraries in Erlang can also start stateful processes when loaded to manage the state of that library, and are thus better thought of as applications.

A good example would be the package Briefly, which generates temporary file paths. It has code / functions that let you ask for a temporary file path, but it also manages in memory process state to ensure that the paths are deleted when no longer in use.

As noted by @Ankhers, applications define a range of acceptable versions for each dependency ( ie ~> 1.0). When you to a mix deps.get for the first time mix will attempt to find a specific version for each application that falls within all the defined versions. If it cannot, you have the option to override the version to be whatever you want, and see if things work anyway.

Most of the time though it isn’t too hard to get a set of versions acceptable to all relevant libraries.

2 Likes

Wow, thanks a lot for all the detailed responses, @benwilson512, @Ankhers, and @michalmuskala – the concepts are much clearer to me now.

I suppose that minor versions (1.0, 1.1, etc.) are never expected to introduce breaking changes then. If I intend to publish an application for consumption by 3rd parties, it is good practice to always define its dependencies in the range format instead of targeting a single specific version?

1 Like

Well, folks are generally expected to follow semver yes, but there’s nothing a package author can do to take code that works right now and make it break. Unlike some package managers mix will never auto-update your dependencies. A mix.lock file is generated the first time you successfully pull down deps that locks in exact versions of every single dependency and sub-dependency. None of them will ever change without you explicitly upgrading them.

This means that even if the author releases a package that goes from 1.1.1 to 1.1.2 and is totally different, it won’t break your existing code because nothing forces you to upgrade at all.

1 Like

For the uninitiated: Semantic Versioning is one of the true pearls of standardization out there.
The idea is extremely simple:

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backwards-compatible manner, and
  3. PATCH version when you make backwards-compatible bug fixes.
    Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

When you follow these rules, then anyone who depends on your library has a strong guarantee about which version of your library could be used together with their library version.

4 Likes