I like to think of the four tools in this article — alias, import, require, and use — as four different ways to reach into another drawer of the toolbox. In the previous articles I organized my functions into drawers and stuck notes on them. Now I need to use tools from other drawers, and each of these four keywords reaches across in its own way. alias is like writing a short nickname on a drawer label so I can point to it quickly. import is like laying the tools I need out on my own workbench, so I can grab them without saying the drawer name every time. require is like flipping the switch that powers a special kind of tool, and use is a helper that flips the switch and lays out the tools for me in a single motion. In this article, I will explore how each one works, how their scope behaves, and when I reach for each.
Note: The examples in this article use Elixir 1.20.1. While most operations should work across different versions, some functionality might vary.
Table of Contents
- Introduction
- Understanding Directives
- Alias: Short Names for Modules
- Import: Bringing Functions Into Scope
- Require: Opting In to Macros
- Use: A Convenient Extension Point
- How the Directives Shape a Namespace
- Practical Guidelines
- Conclusion
- Further Reading
- Next Steps
Introduction
In the article about creating modules, I wrote out fully qualified module names every time I needed a function from another module — things like MyApp.Payments.Processor.process(). That works, but it gets noisy fast. In the article about module attributes, I promised to explore the family of directives that let modules refer to each other more cleanly, and this is that article.
The four tools fall into two groups:
-
Directives —
alias,import, andrequireare lexically scoped, which means they only affect the code block where I write them -
A macro —
useis an extension point that lets another module inject code into mine
What I learned about these tools:
-
aliasshortens a module name — I writeProcessorinstead ofMyApp.Payments.Processor -
importbrings functions into scope — I writeupcaseinstead ofString.upcase -
requireenables macros — a macro cannot run unless its module is required -
usecombines require with a callback — it runs the__using__/1macro of another module - They are all about clarity — each one trades a little explicitness for a little convenience
I found it helpful to hold one question in mind through this whole article: where does this name come from? Every one of these four tools changes the answer to that question in a slightly different way.
Understanding Directives
A Directive Is Lexically Scoped
Before the details, the most important idea: alias, import, and require are directives, which means they are lexically scoped. A directive written at the top of a module applies to the whole module. A directive written inside a function applies only to that function.
Here is the proof, using alias inside a single function:
defmodule Demo.Inner do
def value, do: :found
end
defmodule ScopeDemo do
def inside do
alias Demo.Inner
Inner.value()
end
def outside, do: :no_alias_here
end
Testing in IEx:
iex> ScopeDemo.inside()
:found
iex> ScopeDemo.outside()
:no_alias_here
The alias Inner is only valid inside inside/0. outside/0 has never heard of it. The same rule applies to import and require — I will show both in action shortly.
The Default Scope
When I paste a directive at the top of a module, it applies to every function in that module:
defmodule MyApp.Payments.Processor do
def process(payment_id), do: {:ok, "processed #{payment_id}"}
end
defmodule MyApp.Payments.Report do
alias MyApp.Payments.Processor
def run(payment_id), do: Processor.process(payment_id)
end
Testing in IEx:
iex> MyApp.Payments.Report.run(42)
{:ok, "processed 42"}
The alias on the second line of Report is visible to run/1 because both live at the module level. This is the pattern I use most often.
Reading the Official Definitions
The official guide summarizes the three directives plus use in four one-liners that I kept coming back to:
alias Foo.Bar, as: Bar # refer to Foo.Bar as Bar
require Foo # opt in to using Foo's macros
import Foo # use Foo's functions without the prefix
use Foo # run Foo's custom setup code
Reading them side by side like this made the differences click. Each one answers the question "how do I make names from another module available here?" in a different way.
Alias: Short Names for Modules
The Basic Form
alias creates a short name for a module. Without an :as option, the short name is the last segment of the module name:
alias MyApp.Payments.Processor
is exactly the same as:
alias MyApp.Payments.Processor, as: Processor
After either line, writing Processor anywhere in the same scope means MyApp.Payments.Processor. The short name is not a new module — it is just a pointer to the full one.
Choosing a Different Name with :as
Sometimes the last segment is too generic. If I alias several modules whose names all end the same way, or if the short name would clash with a built-in module, I pick my own name. Using the same Processor from the previous example:
defmodule MyApp.Batch do
alias MyApp.Payments.Processor, as: Pay
def run(payment_id), do: Pay.process(payment_id)
end
Testing in IEx:
iex> MyApp.Batch.run(7)
{:ok, "processed 7"}
I use :as sparingly, mostly when the automatic short name would be misleading. A name like Pay should still tell me what the module is.
Aliasing Several Modules at Once
When I need several modules from the same namespace, I can group them in a single alias with braces. I reuse the Processor alias from above and add a second module to the namespace:
defmodule MyApp.Payments.Invoice do
def create(amount), do: {:ok, "invoiced #{amount}"}
end
defmodule MyApp.Checkout do
alias MyApp.Payments.{Processor, Invoice}
def run(payment_id, amount) do
{Processor.process(payment_id), Invoice.create(amount)}
end
end
Testing in IEx:
iex> MyApp.Checkout.run(1, 100)
{{:ok, "processed 1"}, {:ok, "invoiced 100"}}
The brace syntax expands to two aliases: Processor and Invoice. It reads as "alias both of these from the same drawer".
Nested Aliases
Aliases can build on top of each other. If I alias a whole namespace first, later aliases can be relative to it. Processor and Invoice are already defined above:
defmodule MyApp.NestedCheckout do
alias MyApp.Payments
alias Payments.Processor
alias Payments.Invoice
def run(payment_id, amount) do
{Processor.process(payment_id), Invoice.create(amount)}
end
end
Testing in IEx:
iex> MyApp.NestedCheckout.run(1, 100)
{{:ok, "processed 1"}, {:ok, "invoiced 100"}}
The first line aliases Payments to MyApp.Payments. Then Payments.Processor expands to MyApp.Payments.Processor, and that is aliased as Processor. I like this for modules with a long shared prefix — I write the prefix once and let the rest stay short.
Alias Does Not Check the Module
One detail that surprised me: alias does not verify that the module actually exists. It is a compile-time name resolution, nothing more. This example aliases a module that does not exist and never uses it:
defmodule MyApp.Placeholder do
alias Does.Not.Exist
def run, do: :ok
end
Pasting this prints a warning — and the warning is the lesson:
warning: unused alias Exist
The compiler complains that I aliased a module and never referenced it. It does not complain that Does.Not.Exist is not a real module, because alias never checks. If I later write a call to Exist, the problem shows up at that call site — as a compile warning that the function is undefined, or as a runtime UndefinedFunctionError — never on the alias line. So a typo in an alias is not caught where I write it; it surfaces where I use it.
Alias and Atoms
Under the hood, an alias is still the same atom story from the modules article. Processor in the examples above is just shorthand for the atom :"Elixir.MyApp.Payments.Processor". I do not usually need to think about this, but it explains why aliases work anywhere a module name works — in specs, in function_exported?/3 checks, and in @type definitions.
Import: Bringing Functions Into Scope
The Basic Form
import goes a step further than alias. Instead of shortening the module name, it brings the module's functions into the current scope, so I call them without any prefix at all:
defmodule Text do
def upcase(word), do: String.upcase(word)
def reverse(word), do: String.reverse(word)
def first(word), do: String.first(word)
end
defmodule Shouter do
import Text, only: [upcase: 1]
def shout(word), do: upcase(word)
end
Testing in IEx:
iex> Shouter.shout("hello")
"HELLO"
Inside Shouter, I call upcase/1 directly, as if it were defined in the module itself. The only: [upcase: 1] limits the import to exactly one function, which the official guide recommends doing — importing a whole module brings in far more than I usually want.
Importing Everything Except Some
The opposite option is except:, which imports every public function except the ones I list. Using the same Text module from above:
defmodule Formatter do
import Text, except: [first: 1]
def shout(word), do: upcase(word)
def backwards(word), do: reverse(word)
end
Testing in IEx:
iex> Formatter.shout("hi")
"HI"
iex> Formatter.backwards("hi")
"ih"
upcase/1 and reverse/1 are in scope, but first/1 is not. I prefer only: over except: in my own code, because it makes the list of names I am bringing in explicit.
Functions vs Macros
import can bring in two kinds of things: functions and macros. If a module defines both and I only want one kind, I use only: :functions or only: :macros:
defmodule Mixed do
def double(n), do: n * 2
defmacro even?(n) do
quote do
rem(unquote(n), 2) == 0
end
end
end
defmodule Doubler do
import Mixed, only: :functions
def run(n), do: double(n)
end
defmodule Parity do
import Mixed, only: :macros
def run(n), do: even?(n)
end
Testing in IEx:
iex> Doubler.run(5)
10
iex> Parity.run(4)
true
This touches macros, which get their own article later. For now I only need to know that they are a different kind of importable name, and that only: can filter by kind.
Import Is Lexical Too
Like alias, an import inside a function stays inside that function:
defmodule Scoped do
def shout(word) do
import String, only: [upcase: 1]
upcase(word)
end
def whisper(word) do
import String, only: [downcase: 1]
downcase(word)
end
end
Testing in IEx:
iex> Scoped.shout("hi")
"HI"
iex> Scoped.whisper("HI")
"hi"
Each function imports only what it needs, and neither import leaks into the other. I do not reach for this often — module-level imports are usually clearer — but it is good to know the scope rule holds everywhere.
When Imports Collide
This is the part of import to be careful with. If two modules are imported and both export a function with the same name and arity, the compiler does not guess — it refuses:
defmodule Left do
def compute, do: :from_left
end
defmodule Right do
def compute, do: :from_right
end
defmodule Conflict do
import Left
import Right
def run, do: compute()
end
Pasting this raises a compile error:
error: conflicting compute/0 import from modules Right and Left
There is also a subtler hazard: an import can shadow a function from Kernel, which is imported by default in every module. Importing a module that defines its own length/1 or max/2 silently overrides the familiar one. Both hazards are why the guide recommends only: with a small explicit list, and why I mostly prefer alias over import in my own code — Processor.process() tells me where process comes from; process() does not.
Require: Opting In to Macros
Why Macros Need Require
Public functions are globally available — I can call String.upcase/1 anywhere without any setup. Macros are different: because a macro expands at compile time, the compiler needs its module already loaded and compiled. require is how I opt in.
Integer.is_even/1 is the classic example, because it is defined as a macro so it can be used inside guards. If I call it without requiring, I get a helpful error:
iex> Integer.is_even(4)
** (UndefinedFunctionError) function Integer.is_even/1 is undefined or private. However, there is a macro with the same name and arity. Be sure to require Integer if you intend to invoke this macro
The error message does the teaching for me — it even tells me exactly what to do:
iex> require Integer
Integer
iex> Integer.is_even(4)
true
Require Inside a Module
In a real module, I put require at the top, just like alias and import:
defmodule EvenChecker do
require Integer
def even?(n), do: Integer.is_even(n)
end
Testing in IEx:
iex> EvenChecker.even?(4)
true
iex> EvenChecker.even?(3)
false
Note that require does not shorten the name — I still call Integer.is_even/1 with the full module name. require only unlocks the macro. If I want the short name too, I pair it with alias or import.
Require Is Lexical
Like the other directives, require is lexically scoped. Requiring a module inside a function makes its macros available only inside that function:
defmodule Checker do
defmacro is_even?(n) do
quote do
rem(unquote(n), 2) == 0
end
end
end
defmodule Local do
def describe(n) do
require Checker
Checker.is_even?(n)
end
def no_macro, do: :ok
end
Testing in IEx:
iex> Local.describe(4)
true
iex> Local.describe(3)
false
The require lives inside describe/1, so only that function can call the is_even?/1 macro; no_macro/0 has no access to it. In practice I put require at the module level, because macros I use once I usually use in several functions.
Use: A Convenient Extension Point
What Use Actually Does
use is the odd one out — it is not a directive but a macro, and it does more than any of the other three. In pseudo-code — Feature here is just a placeholder, not a real module — when I write:
defmodule Example do
use Feature, option: :value
end
the compiler expands it to roughly this:
defmodule Example do
require Feature
Feature.__using__(option: :value)
end
So use first requires the module, then calls its __using__/1 macro, passing along any options. That macro is free to inject whatever code it wants into my module — imports, aliases, function definitions, module state. This is why the guide warns to read a module's documentation before using it: use runs arbitrary code.
A Small Example
To see the mechanism, let me define a tiny module with a __using__/1 macro:
defmodule Greeting do
defmacro __using__(opts) do
greeting = Keyword.get(opts, :greeting, "Hello")
quote do
def greet(name), do: unquote(greeting) <> ", " <> name <> "!"
end
end
end
Now I can "use" it and pick up a greet/1 function, customized through the options:
defmodule English do
use Greeting
end
defmodule Portuguese do
use Greeting, greeting: "Olá"
end
Testing in IEx:
iex> English.greet("John")
"Hello, John!"
iex> Portuguese.greet("João")
"Olá, João!"
The __using__/1 macro returned a quoted function definition, and use dropped that definition into each module. English got the default greeting, Portuguese passed its own. This is the same mechanism behind the framework modules I already know — use ExUnit.Case, use GenServer — which use it to inject a whole set of behavior into my module.
How Use Differs From Require
The distinction cleared up once I saw the expansion: require Feature only makes Feature's macros callable. It does not run anything. use Feature both requires and runs Feature.__using__/1, which is what actually injects code. That is a big difference in side effects — require is harmless, use is a small program running inside my module.
The guide's advice stayed with me: do not use use where an alias or import would do. If I only need a shorter name or a function in scope, I reach for those. I save use for real extension points — modules that are designed to set up behavior in mine.
How the Directives Shape a Namespace
Putting Them Together
A realistic module might use all three directives at once, each for its own reason. MyApp.Payments.Processor is still defined from earlier, so I only add the logger and the worker here:
defmodule MyApp.Logger do
def log(message), do: IO.puts("[log] #{message}")
end
defmodule MyApp.Worker do
alias MyApp.Payments.Processor
import MyApp.Logger, only: [log: 1]
require Integer
def run(payment_id) do
log("starting #{payment_id}")
if Integer.is_even(payment_id) do
Processor.process(payment_id)
else
:skip
end
end
end
Testing in IEx:
iex> MyApp.Worker.run(4)
[log] starting 4
{:ok, "processed 4"}
iex> MyApp.Worker.run(3)
[log] starting 3
:skip
Three different jobs, three different tools: alias for the module I call repeatedly, import for the logging helper I want bare, and require for the macro. The log/1 helper prints a line and returns :ok, so each run shows the log message followed by the result. Reading this module, I can trace every bare name to its origin without much effort.
Reading a Namespace
After learning these tools, I started reading code differently. When I see a bare function name, I ask whether it was defined in the module, imported, or comes from Kernel. When I see a short module name, I look for the alias at the top. This little ritual is what made the whole article worth it — the directives are not just conveniences, they are the map of how a module reaches into the rest of the codebase.
The trade-off underneath all of them is the same: alias and import save typing but move the origin of a name further away from the call site. require and use unlock power but add hidden steps. A clean module keeps that trade-off visible.
Practical Guidelines
I prefer alias over import in my own code — Processor.process() keeps the origin of the function visible, and it avoids the collision and shadowing hazards of imports.
I use only: whenever I import — a short explicit list of names tells the reader exactly what came from where and prevents accidental shadowing of Kernel functions.
I use :as only for clarity — the automatic short name is usually right; I override it when it would be vague or clash.
I require only modules whose macros I actually call — require has no effect on plain function calls, so there is no reason to add it otherwise.
I reserve use for extension points — if alias or import would do the job, that is the tool. I use use when a module is designed to inject behavior, and I read its docs first.
I keep directives at the top of the module — a small block of alias and import lines acts like a table of contents, showing me at a glance which other drawers this module reaches into.
Conclusion
These four tools turned out to be less about saving keystrokes and more about making the connections between modules explicit. Each one controls how a name from another drawer becomes available in mine, and each has a clear job once I learned to tell them apart.
Some things I learned:
-
Directives are lexically scoped —
alias,import, andrequireaffect only the block where they appear -
aliasshortens names — it points a short name at a full module name without checking that the module exists -
importbrings functions in — it makes names callable without a prefix, which is powerful but collision-prone -
requireunlocks macros — macros must be required before use because they run at compile time -
useis an extension point — it requires a module and runs its__using__/1, injecting code into mine
The toolbox picture held together well: alias is the nickname on the drawer, import is tools spread out on my workbench, require is the power switch, and use is the helper that does the whole setup for me. The line I keep now is: if a bare name appears in a function, I should be able to point to where it came from without much thinking.
Further Reading
- Elixir Official Documentation — Alias, require, import, and use
- Elixir Official Documentation — Kernel.use/2
Next Steps
With modules, attributes, and now the directives that connect modules, I have the building blocks for organizing code. The natural next step is zooming out to see how a whole project is arranged on disk — the directories, the naming rules, and the boundaries that keep a growing codebase navigable.
In the next article, we will explore:
- How a Mix project is laid out, starting from
mix new - What belongs in the
lib,test,priv, andconfigdirectories - Naming modules so they mirror the directory structure
- Introducing namespaces and boundaries as a project grows
- Configuration files and how environments (dev, test, prod) fit in
These ideas tie directly into everything in the last few articles — once modules are organized, attributed, and connected with directives, the question becomes where they all live and how they stay tidy at scale.
A small but important change is coming too: this was the last article driven by pasting examples into iex. From the next article on, we will create a real Mix project and write examples as files in it, running them with mix run and, later, mix test.
Top comments (0)