Skip to content

Instantly share code, notes, and snippets.

@TylerPachal
Last active September 26, 2023 15:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TylerPachal/66e9e7f42d1ff75c9a47c55deb6a7e5d to your computer and use it in GitHub Desktop.
Save TylerPachal/66e9e7f42d1ff75c9a47c55deb6a7e5d to your computer and use it in GitHub Desktop.
A short example of emitting custom compile-time warnings in Elixir using IO.warn/1

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

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment