Nested Modules and Auto-Aliasing in Elixir
Recently, I was coding a module to create CSV files from database records when I came across an unexpected error in Elixir. Using the CSV library, I wrote something like this:
# mix.exs
defp deps do
[
{:csv, "~> 1.4.2"}
...
]
end
# csv.ex
defmodule Foo do
defmodule CSV do
def export(file) do
CSV.encode(file)
end
end
end
I expected Foo.CSV.export(file)
to trigger the CSV
library to encode the file I passed into the function. To my surprise, the app came back with this error:
(UndefinedFunctionError) function Foo.CSV.encode/1 is undefined
or private
This error kind of makes sense. I’m calling a function on a CSV
module, and I just defined a named CSV
module right above it. Naturally, the app should be confused as to which CSV
module I’m referring to. But I really didn’t want to change my module’s name, so how can I tell the app which module I want to use?
After some experimenting, I found something even more interesting - switching from a nested module to a single namespaced module made it work:
# csv.ex
defmodule Foo.CSV do
def export(file) do
CSV.encode(file)
end
end
What?? I thought nested modules and namespaced modules compiled to the same thing? Shouldn’t the first example also compile to Foo.CSV
? And why does the second example not throw the same error as the first one?
Why this happens
After asking around, I found this behavior puzzled other Elixir developers as well. Thankfully, Bryan Joseph helped me figure out what was happening and also pointed me to part in the docs that explains this behavior.
While compiling, when Elixir reaches a nested module, it creates an “auto-alias” for that nested module. In other words, this code:
defmodule Foo do
defmodule CSV do
def export(file) do
CSV.encode(file)
end
end
end
actually compiles to something more like this behind the scenes:
defmodule Foo do
defmodule Foo.CSV do
def export(file) do
CSV.encode(file)
end
end
alias Foo.CSV, as: CSV
end
This is why my original example threw an error - when CSV.encode(file)
is called, the auto-alias directs the app to instead look for an encode/1
function on the Elixir.Foo.CSV
module.
Yet, when modules are not nested, the compiler does not create an auto-alias, so there is no confusion about which CSV
module I’m trying to reference.
So if you are hitting a similar error with nested modules, consider either changing the name or switching over to a namespaced module instead.