Custom Compile-Time Warnings in Elixir Using IO.warn/1
❗The original blogpost lives on Github where the Elixir formatting is nicer than it is here on Medium❗
When you are writing macros which run at compile-time, it can be useful to emit custom compile-time warnings.
Example Scenario
Say you are using a text file to dynamically define function clauses. Our function will be called official_language/1
; its only argument is the name of a country, and it returns an :ok
tuple with a list of the countries official languages.
Fortunately, for our implementation we have been provided with a official_languages.csv
file that contains the mappings we need to perform this lookup:
Belgium,"Dutch,French,German"
Brazil,Portugese
Canada,"English,French"
Mexico,Spanish
Italy,Italian
Using the contents of this file to dynamically define our function with metaprogamming would look like this:
defmodule Country do
@external_resource "priv/official_languages.csv"
@contents File.read!("priv/official_languages.csv")
rows = NimbleCSV.RFC4180.parse_string(@contents)
for [country, languages] <- rows do
languages_list = String.split(languages, ",")
def official_language(unquote(country)) do
{:ok, unquote(languages_list)}
end
end
def official_language(_country), do: {:error, :unknown_country}
end
(Read more about @external_resource
here)
With the CSV file in place, you’d be able to make the following function calls:
$ iex -S mix
iex(1)> Country.official_language("Canada")
{:ok, ["English", "French"]}
iex(2)> Country.official_language("Tylerland")
{:error, :unknown_country}
The Problem
What if there was accidentally a duplicate country in your CSV file? Maybe the file contained an additional (and incorrect) line: Brazil,Spanish
.
We’d see a compiler warning, which is not helpful at narrowing down which country was the duplicate:
$ mix compile --force
Compiling 1 file (.ex)
warning: this clause cannot match because a previous clause at line 8 always matches
lib/country.ex:10
The Solution
It would be better if we emitted a more helpful error message (after identifying non-unique country keys). This can be done using IO.warn/1
:
defmodule Country do
@external_resource "priv/official_languages.csv"
@contents File.read!("priv/official_languages.csv")
rows = NimbleCSV.RFC4180.parse_string(@contents)
# New part here!
countries = Enum.map(rows, fn [country, _] -> country end)
non_unique_countries = countries -- Enum.uniq(countries)
if non_unique_countries != [] do
IO.warn("Non-unique country in #{@external_resource}: #{inspect(non_unique_countries)}")
end
for [country, languages] <- rows do
languages_list = String.split(languages, ",")
def official_language(unquote(country)) do
{:ok, unquote(languages_list)}
end
end
def official_language(_country), do: {:error, :unknown_country}
end
Now when we compile the code, we’ll get a more helpful warning preceding the warning from the Elixir compiler:
$ mix compile --force
Compiling 1 file (.ex)
warning: Non-unique country in priv/official_languages.csv: ["Brazil"]
lib/playground.ex:11: (module)
(elixir 1.13.3) src/elixir_compiler.erl:73: :elixir_compiler.dispatch/4
(elixir 1.13.3) src/elixir_compiler.erl:58: :elixir_compiler.compile/3
(elixir 1.13.3) src/elixir_module.erl:369: :elixir_module.eval_form/6
(elixir 1.13.3) src/elixir_module.erl:105: :elixir_module.compile/5
(elixir 1.13.3) src/elixir_lexical.erl:15: :elixir_lexical.run/3
warning: this clause cannot match because a previous clause at line 19 always matches
lib/playground.ex:19