5 min read
Three Ways to Consume Code
When to Use require, import, and alias in Elixir

When you start writing Elixir, the pattern is simple: you prefix everything with the module name. Integer.is_odd. Enum.map. String.split. It’s readable, explicit, and mostly just works.

Then at some point you write something like this:

Enum.filter(1..10, &Integer.is_odd/1)

And Elixir tells you: no function Integer.is_odd exists. There is a macro with that name, but you haven’t opted into it.

That error is your first encounter with the distinction Elixir makes between its three module directives: require, import, and alias. They all bring external code within reach, but each solves a different problem.

Why three?

The natural question is: why does Elixir need three ways to consume code?

Because consuming code is not one decision.

Sometimes you need permission to use something that can reshape your code at compile time. Sometimes you want functions from another module to feel local. Sometimes the module name is just too long to repeat twenty times in the same file. Those are different pressures. Elixir keeps them separate so your code says which one you’re choosing.

The default remains fully-qualified names, and that default is doing real work. Enum.map tells you more than map. MyApp.Accounts.User.changeset tells you more than changeset. The module prefix isn’t noise until it gets in the way. It’s part of the explanation.

🎯 Join Groxio's Newsletter

Weekly lessons on Elixir, system design, and AI-assisted development -- plus stories from our training and mentoring sessions.

We respect your privacy. No spam, unsubscribe anytime.


require: opting into macros

Macros are code that writes code. They run at compile time and can do things functions cannot. That is useful, but it also means macros deserve a little more ceremony.

Elixir makes that ceremony explicit. Bruce says it this way in class:

“One of the rules in Elixir is that if you’re going to consume a macro, you have to opt in. And the way that you opt in is with require.” — Bruce Tate

That is what happens with Integer.is_odd/1. This fails:

Enum.filter(1..10, &Integer.is_odd/1)
# ** (UndefinedFunctionError) function Integer.is_odd/1 is undefined or private.
# However there is a macro with the same name and arity.

The fix is not to import Integer. The fix is to require it:

require Integer

Enum.filter(1..10, &Integer.is_odd/1)
# [1, 3, 5, 7, 9]

require does exactly one thing: it lets the current module use macros from another module. It doesn’t bring names into local scope, and it doesn’t shorten anything. If a macro can generate code on your behalf, Elixir wants the call site to show that you agreed to it.

import: making functions feel local

import changes the reading experience more dramatically. When you import a module, you bring its public functions into the current scope. Bruce summarizes it directly: “Import brings in public functions as if they had been declared locally.”

That can be convenient, but the cost is real. The shorter call is easier to type and harder to trace.

map(users, &normalize_user/1)

Where does map come from? Is it local, imported from Enum, defined by a helper? The reader has to look around. Compare that with:

Enum.map(users, &normalize_user/1)

There is less to infer. The name tells you where to look.

This doesn’t make import wrong. It makes it expensive. If a prefix is repeated so often that it damages the expression you’re trying to write, import can help. But prefer importing only what you need:

import Integer, only: [is_odd: 1]

That keeps the local namespace small. The wider the import, the more common names lose their origin: new, parse, build — many modules reasonably define those.

alias: shortening names without hiding them

alias is the directive you’ll reach for most. It doesn’t grant macro access and doesn’t bring functions into local scope. It renames a module so you can refer to it with a shorter name. Bruce’s description covers it entirely: “Alias is nothing more than a rename, usually for manageability and reduced ceremony.”

alias MyApp.Accounts.User

User.changeset(user, attrs)

By default, Elixir takes the last segment of the module name. If you want something different:

alias My.Long.Module, as: M

This is why alias carries less risk than import. The module name stays visible. You shortened it, but you didn’t erase the fact that a module is involved. When you read User.changeset, you still know you’re calling a function on a module. That visibility matters as a codebase grows.

The rule of thumb

Elixir starts from clarity. Fully-qualified names are the default because they make origin explicit. The directives are exceptions you choose when some other pressure matters more: require asks for nothing except your acknowledgment of a macro, import trades origin visibility for brevity, and alias trades length for nothing — the module stays visible, just shorter.

Once you see them this way, they stop feeling like syntax to memorize. They become small readability decisions: opt into macros, localize functions, or shorten module names.

In the next post, we’ll look at how Elixir programs actually run: processes, messages, and the actor model that makes Elixir a different kind of concurrent language.

See you in the next chapter.


Want to Learn Elixir the Way It's Actually Designed?

This post comes from Bruce's structured Elixir course, where you build the mental models behind modules, macros, and how Elixir code stays readable as it grows. Learn the language through real-world scenarios that make each design choice feel intentional instead of surprising.

— Paulo & Bruce

Bruce Tate's avatar
Bruce Tate
System architecture expert and author of 10+ Elixir books.
Paulo Valim's avatar
Paulo Valim
Full-stack Elixir developer and educator teaching modern Elixir and AI-assisted development.