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
:
...
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.