Skip to content
Dimitrios
August 29, 2024

Elixir: Import, Require, Use or Alias?

Importing and aliasing in Elixir

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.

lock_unverified_accounts_worker
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
end

We 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.

Untitled-1
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.

lock_unverified_accounts_worker
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
end

There'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 :

Untitled-1
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
end

And let's put it to use in our worker module.

lock_unverified_accounts_worker
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
end

Everything 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.

Untitled-1
defmodule MyApp.Helpers.LogThisPlease do
  defmacro __using__(_opts) do
    quote do
      def info(message) do
        IO.puts "[INFO] #{message}"
      end
    end
  end
end

Now, we can use this module in our worker.

lock_unverified_accounts_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
end

By 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
  1. Alias provides shorter names for modules, making them easier to reference.
  2. Import brings specific or all functions and macros into the current scope.
  3. Require ensures a module is loaded and is necessary for using macros.
  4. Use invokes the __using__/1 macro of another module, often used for setting up behaviors or injecting code.