DEV Community

Cover image for Learning Elixir: Alias, Import, Require and Use
João Paulo Abreu
João Paulo Abreu

Posted on

Learning Elixir: Alias, Import, Require and Use

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

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:

  • Directivesalias, import, and require are lexically scoped, which means they only affect the code block where I write them
  • A macrouse is an extension point that lets another module inject code into mine

What I learned about these tools:

  • alias shortens a module name — I write Processor instead of MyApp.Payments.Processor
  • import brings functions into scope — I write upcase instead of String.upcase
  • require enables macros — a macro cannot run unless its module is required
  • use combines require with a callback — it runs the __using__/1 macro 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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> ScopeDemo.inside()
:found

iex> ScopeDemo.outside()
:no_alias_here
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> MyApp.Payments.Report.run(42)
{:ok, "processed 42"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

is exactly the same as:

alias MyApp.Payments.Processor, as: Processor
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> MyApp.Batch.run(7)
{:ok, "processed 7"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> MyApp.Checkout.run(1, 100)
{{:ok, "processed 1"}, {:ok, "invoiced 100"}}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> MyApp.NestedCheckout.run(1, 100)
{{:ok, "processed 1"}, {:ok, "invoiced 100"}}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Pasting this prints a warning — and the warning is the lesson:

warning: unused alias Exist
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> Shouter.shout("hello")
"HELLO"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> Formatter.shout("hi")
"HI"

iex> Formatter.backwards("hi")
"ih"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> Doubler.run(5)
10

iex> Parity.run(4)
true
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> Scoped.shout("hi")
"HI"

iex> Scoped.whisper("HI")
"hi"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Pasting this raises a compile error:

error: conflicting compute/0 import from modules Right and Left
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> EvenChecker.even?(4)
true

iex> EvenChecker.even?(3)
false
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> Local.describe(4)
true

iex> Local.describe(3)
false
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

the compiler expands it to roughly this:

defmodule Example do
  require Feature
  Feature.__using__(option: :value)
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> English.greet("John")
"Hello, John!"

iex> Portuguese.greet("João")
"Olá, João!"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> MyApp.Worker.run(4)
[log] starting 4
{:ok, "processed 4"}

iex> MyApp.Worker.run(3)
[log] starting 3
:skip
Enter fullscreen mode Exit fullscreen mode

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 codeProcessor.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 callrequire 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 scopedalias, import, and require affect only the block where they appear
  • alias shortens names — it points a short name at a full module name without checking that the module exists
  • import brings functions in — it makes names callable without a prefix, which is powerful but collision-prone
  • require unlocks macros — macros must be required before use because they run at compile time
  • use is 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

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, and config directories
  • 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)