Coming from a decade of work in JavaScript, having these four different directives in Elixir felt strange at first. Wait, what? use is not just a directive, but actually a macro? Ok, things are really confusing.
Let’s try to clarify them.
Alias
alias allows us to use a shorter name for a module, making it easier to reference in the code. So, when a module has a long (nested) name, we can omit the full name by using an alias. Instead, we only need to type the last part of the module's name.
defmodule MyApp.Workers.LockUnverifiedAccountsWorker do
use Oban.Worker, queue: :unverified_accounts
# Let’s look at how to use alias
alias MyApp.Domain.Accounts
alias MyApp.Services.AdminNotifications
@impl Oban.Worker
def perform(_job) do
# Using `Accounts` instead of `MyApp.Domain.Accounts`
{locked_count, locked_users} = Accounts.lock_expired_unverified_accounts()
for user <- locked_users do
# Using `AdminNotifications` instead of `MyApp.Services.AdminNotifications`
AdminNotifications.notify(:user_locked, user)
end
{:ok, locked_count}
end
endWe can also specify a custom alias using the :as option. This can be useful when we have multiple modules with the same name or when we want to give the alias a more descriptive name.
alias MyApp.Services.AdminNotifications, as: Snitch
Snitch.notify()Pretty straightforward. With alias we can remove the extra baggage of long module names and make our code more readable. More about aliasing in Elixir can be found here.
Import
import allows us to bring functions or macros (more on macros later) from another module into the current scope without prefixing them with the module name.
Let’s see how we can use import in the same example.
defmodule MyApp.Workers.LockUnverifiedAccountsWorker do
use Oban.Worker, queue: :unverified_accounts
# Let’s focus on these again
import MyApp.Domain.Accounts, only: [lock_expired_unverified_accounts: 0] # `0` means we are importing the function with 0 arity, i.e. no arguments.
import MyApp.Services.AdminNotifications
@impl Oban.Worker
def perform(_job) do
# No need to prefix with `Accounts`.
# We only imported `lock_expired_unverified_accounts` from it
{locked_count, locked_users} = lock_expired_unverified_accounts()
for user <- locked_users do
# No need to prefix with `AdminNotifications`,
# but as a side-effect, we have access to all its functions, for better or worse
notify(:user_locked, user)
end
{:ok, locked_count}
end
endThere’s a downside to using import, though. Sometimes it’s not immediately clear where a function is coming from. So if you’re using import, opt for only or except to avoid naming conflicts. In this example everything that AdminNotifications has, is now available in the current scope, which can be a bit dangerous.
I try to avoid import but make an exception for tests. For example, in my tests, I don’t mind importing all the fixtures I wrote, but in the actual code, I prefer explicitness, even if things get a bit lengthy.
Read more about import here.
Require
Macros are a metaprogramming primitive; code that generates code. They are executed at compile time, contrary to functions that get called at runtime.
So, if we want to use a module with macros, we need to require it first so it will be available at compile time.
Let’s create a simple module with a macro :
defmodule MyApp.Helpers.LogThisPlease do
# In our compiled code, this will be replaced with `IO.puts("Hello, world!")`
# A normal function instead, would be referenced and executed at the runtime
defmacro info(message) do
quote do
IO.puts "[INFO] #{unquote(message)}"
end
end
endAnd let’s put it to use in our worker module.
defmodule MyApp.Workers.LockUnverifiedAccountsWorker do
use Oban.Worker, queue: :unverified_accounts
alias MyApp.Domain.Accounts
alias MyApp.Services.AdminNotifications
# We need to require the module with the macro
require MyApp.Helpers.LogThisPlease
@impl Oban.Worker
def perform(_job) do
{locked_count, locked_users} = Accounts.lock_expired_unverified_accounts()
# Using the full name of the module
MyApp.Helpers.LogThisPlease.info("Found #{locked_count} users to lock")
MyApp.Helpers.LogThisPlease.info("Locking users")
for user <- locked_users do
AdminNotifications.notify(:user_locked, user)
MyApp.Helpers.LogThisPlease.info("User locked: #{user.id}")
end
{:ok, locked_count}
end
endEverything works, but it looks pretty busy.
If we want, we can replace require with import. By doing this import pulls everything from the module into the current scope, and implicitly runs require for us.
This isn’t true for alias as that only creates a shorter alias for the module, and we still need to require it ourselves.
More about require can be found here.
Use
Speaking of macros, use is a macro itself. It invokes the __using__/1 macro of another module. This is often used to set up behaviors or inject code into the current module.
That's a bit abstract, so let’s revisit our previous example.
defmodule MyApp.Helpers.LogThisPlease do
defmacro __using__(_opts) do
quote do
def info(message) do
IO.puts "[INFO] #{message}"
end
end
end
endNow, we can use this module in our worker.
defmodule MyApp.Workers.LockUnverifiedAccountsWorker do
use Oban.Worker, queue: :unverified_accounts
alias MyApp.Domain.Accounts
alias MyApp.Services.AdminNotifications
use MyApp.Helpers.LogThisPlease
@impl Oban.Worker
def perform(_job) do
{locked_count, locked_users} = Accounts.lock_expired_unverified_accounts()
info("Found #{locked_count} users to lock")
info("Locking users")
for user <- locked_users do
AdminNotifications.notify(:user_locked, user)
info("User locked: #{user.id}")
end
{:ok, locked_count}
end
endBy doing this, we eliminated the need to call MyApp.Helpers.LogThisPlease.info and can now call info directly. It is a typical pattern in Elixir, especially in libraries. For example, Oban uses use Oban.Worker to set up all the necessary plumbing to make this module work as an Oban job.
Of course, it poses the same problem as import - it’s not immediately clear where the function comes from.
TL;DR
- Alias provides shorter names for modules, making them easier to reference.
- Import brings specific or all functions and macros into the current scope.
- Require ensures a module is loaded and is necessary for using macros.
- Use invokes the
__using__/1macro of another module, often used for setting up behaviors or injecting code.