Recently on a project there was a need to validate currencies.

One solution would be writing a function to check if a given string exists in a list of currencies. One way to do that would be:

@currencies ["NGN", "USD", ...]

def valid?(currency), do: currency in currencies

But, I didn't want to write the @currencies list by hand as there are over 200 currencies available.

In Elixir, we can generate function at compile-time. This reduces the time & effort needed to create functionalities with no run-time penalties.

We can write simple functions to handle one currency each using function clauses:

# Preferred approach:
def valid?("NGN"), do: true
def valid?("USD"), do: true
def valid(_), do: false

# Another approach using guards:

def valid?(currency_code), where currency_code == "NGN", do: true
def valid?(currency_code), where currency_code == "USD", do: true
def valid?(_), do: false

If we were to write that by hand, we would write over 200 function clauses which will create a very long source file.

We store all currencies in a CSV file in the format:

Nigeria, Naira, NGN
United States of America, US Dollar, USD
...

We're not interested in the country names at the moment, but we left them in for completeness. Our module now becomes:

defmodule X.Currency do
  @currency_tuples File.read!("currencies.csv")
      |> String.split("\n")
      |> Enum.map(&String.trim/1)
      |> Enum.map(&String.split(&1, ","))
      |> Enum.map(fn [_ | rest] -> {Enum.at(rest, -2), Enum.at(rest, -1)} end) # {currency, code}
      |> Enum.filter(fn
        {_, ""} -> false
        {_, nil} -> false
        {_, _} -> true
      end)
      |> Enum.uniq


  @spec valid?(binary) :: true | false
  @spec list :: %{binary => binary}

  @currency_tuples
  |> Enum.each(fn {_, code} ->
    def valid?(unquote(code)), do: true
  end)

  def valid?(_), do: false

  def list, do: Map.new(@currency_tuples)
end

During compilation, the compiler evaluates @currency_tuples module attribute first. The steps involved in this evaluation are:

  • Reading the CSV file
  • Splitting the string into rows
  • Splitting rows into columns
  • Taking only the columns we're interested in (currency name and code)
  • Removing empty records; and,
  • Removing duplicates (more than one country uses the US Dollar).

After this is done, the actual function generation is done in this snippet:

  ...
  @currency_tuples
  |> Enum.each(fn {_, code} ->
    def valid?(unquote(code)), do: true
  end)
  ...

For each currency in our list, we create a function that returns true. unquote(code) replaces code with the contents of the variable (i.e. NGN, USD, etc).

Of course, you can change the function generation line can to use the second approach if you prefer.

The generated code after compilation is identical to what the compiler would produce if we wrote  all the functions by hand.

Basically, we got Elixir to write our functions for us before compiling the module.

Why write functions when you can get your tools to write them for you?