Soft dependencies in Elixir projects

How to support soft dependencies in Elixir libraries to provide optional features without any dependency baggage?

Easily!

I will tell you how I added support for decimals in the Elixir Money library. It surprised me how easy this was.

Soft vs hard dependencies

Soft dependencies (unlike hard dependencies) are optional. We can build libraries with many features, but let the user decide what she wants. In such a case, if she wants to use our feature F that needs an extra dependency on D, the D dependency has to be added in the user project (rather than being required by default).

As a real example, I want to show you how I added support for Decimal in the Money library.

Money provides parse/3 and parse!/3 functions to get you started:

iex> Money.parse("$1,234.56", :USD)

In the project I was working on we save money values as decimals. So what I wanted was to be pass was a decimal in the Money.parse/2 function and also get a decimal back using Money.to_decimal/1.

All of that without polluting current Money users with the Decimal dependency. So how to do it?

Implementing a soft dependency

In the mix.exs file we can define our dependency as optional by setting the optional flag to true:

  ...
  {:decimal, "~> 1.0", optional: true},
  ...

This ensures that the projects consuming Money library won’t be getting this dependency by default, but it’s included for development and testing of the library itself.

Then we use Code.ensure_compiled? function to wrap any code that depends on Decimal:

I wrote previously:
  ...
  if Code.ensure_compiled?(Decimal) do
    def parse(%Decimal{} = decimal, currency, _opts) do
      Decimal.cast(decimal) |> Decimal.to_float() |> Money.parse(currency)
    end
  end
  ...

That’s all it takes. See my full pull request.

Any comments? Write me a DM on Twitter.

Before you leave…

I am writing an introductory book on web application deployment. Networking, processes, systemd, backups, and all your usual suspects.

Check it out →