Custom Compile-Time Warnings in Elixir Using IO.warn/1 --- When you are writing macros which run at compile-time, it can be useful to emit custom compile-time warnings. [Skip down to The Solution](#the-solution) ### 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: ```text 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: ```elixir 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](https://hexdocs.pm/elixir/1.12.3/Module.html#module-external_resource))_ With the CSV file in place, you'd be able to make the following function calls: ```bash $ 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: ```bash $ 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`](https://hexdocs.pm/elixir/1.12.3/IO.html#warn/1): ```elixir 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: ```bash $ 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 ```