Custom Compile-Time Warnings in Elixir Using IO.warn/1

Tyler Pachal
2 min readSep 19, 2023

❗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

--

--