Elixir Modules Explained - Organize and Structure Your Code

Learn how to use Elixir modules to organize your code, define functions, and manage namespaces effectively. Understand module declarations, function definitions, private functions, and more.

Elixir Modules

Understanding Elixir Modules

Elixir modules are fundamental for organizing code under a namespace. They allow developers to group related functions and data, promoting modularity and maintainability. While modules can be meta-programmed, their definitions are fixed at compile time and cannot be altered during runtime; they can only be replaced.

Modules are named according to a specific convention to ensure clarity and avoid conflicts. The standard format for module names is:

Elixir Module Naming Convention

Declaring and Defining Module Functions

A module is declared using the defmodule keyword, followed by the module name and a do...end block. Inside this block, functions are defined using def. Function names must start with a lowercase letter (a-z) and can include uppercase letters, numbers, and underscores. They may optionally end with a question mark (?) for predicates or an exclamation mark (!) for functions that may have side effects or raise errors.

defmodule MyModule do
end

Defining Module Functions

Functions within a module are defined using def. The def keyword is actually a macro. Similar to other macro calls, the do...end block for a function definition can be written concisely on a single line using do:.

defmodule MyModule do
  def my_function do
    IO.puts("Hello from my function")
  end
end

A one-liner definition:

defmodule MyModule do
  def my_function, do: IO.puts("Hello from my function")
end

Calling Module Functions

Functions defined within a module can be called directly by their name if the call is made from within the same module. However, when calling a function from outside its defining module, you must qualify the function name with the module's name, separated by a dot (.). For example, IO.puts() is called from outside the IO module.

defmodule MyModule do
  def function1 do
    IO.puts "func 1"
  end
  def function2 do
    function1
    IO.puts "funct 2"
  end
end

# Calling function2 from outside MyModule
# > MyModule.function2

Function Arguments and Defaults

Arguments are passed to Elixir functions positionally. Default values can be assigned to arguments, making them optional. Arguments can be of any data type.

defmodule MyModule do
  # The 'who' argument defaults to "earthlings" if not provided
  def greet(greeting, who \\ "earthlings") do
    IO.puts("#{greeting} #{who}")
  end
end

# Calling with both arguments
# > MyModule.greet("'sup", "y'all?")  # Output: "'sup y'all?"

# Calling with only the required argument
# > MyModule.greet("greetings")       # Output: "greetings earthlings"

Function Overloading with Pattern Matching

Elixir supports defining multiple function clauses with the same name but different argument patterns. This allows for a form of overloading, where the most specific matching clause is executed. This is particularly useful for handling different input types or states.

defmodule MyModule do
  # Default function clause
  def greet() do
    greet("hello", "you")
  end

  # Function clause with specific arguments
  def greet(greeting, who) do
    IO.puts("#{greeting} #{who}")
  end
end

# Example usage:
# > MyModule.greet("hello")  # Output: "hello you"
# > MyModule.greet()         # Output: "hello you" (calls the default greet)

Pattern matching can also be used to handle specific values or ignore arguments.

def is_it_the_number_2?(2) do
  true
end

# The underscore '_' ignores the argument value
def is_it_the_number_2?(_) do
  false
end

Guards in Function Definitions

Function definitions can be augmented with guards using the when keyword. Guards provide additional conditions that must be met for a function clause to be selected. This allows for more precise control over function execution based on argument properties.

def square(n) when is_number(n), do: n * n
def square(_), do: raise "Input must be a number"

Private Functions

To define functions that are only accessible within the defining module, use defp instead of def. This helps in encapsulating implementation details and preventing unintended external access.

defmodule ModA do
  # Private function
  defp hi, do: IO.puts "Hello from ModA"

  # Public function that calls the private function
  def say_hi, do: hi
end

# Calling the public function works
# ModA.say_hi
# Output: Hello from ModA

# Calling the private function from outside results in an error
# ModA.hi
# ** (UndefinedFunctionError) undefined function ModA.hi/0
# ModA.hi()

Interacting with Other Modules

Elixir provides several directives to manage how modules interact with each other:

import

import SomeModule brings all functions and macros from SomeModule into the current module's scope, allowing them to be called without the module name prefix. The import directive can be refined using only: or except: options to specify which functions or macros to include or exclude.

defmodule ModA do
  # Import all functions and macros from ModB
  import ModB

  # Import all except destroy_planet/1
  import ModB, except: [destroy_planet: 1]

  # Import only functions from ModB, excluding macros
  import ModB, only: :functions

  # Import only specific functions or macros
  import ModB, only: [say_hi: 0, fibonacci: 1]
end

require

require SomeModule ensures that SomeModule is compiled before the current module and allows you to use macros defined in SomeModule. This is crucial for using macro-based libraries.

use

use SomeModule first requires SomeModule and then executes the SomeModule.__using__/1 macro. This directive is commonly used for setting up metaprogramming contexts or applying predefined behaviors.

alias

alias SomeVery.Long.ModuleName, as: SVLMN is used to create a shorter, more convenient alias for a module name. This reduces verbosity when referring to long module names repeatedly.

Module Attributes

Module attributes are pieces of data, akin to metadata or constants, associated with a module. They are inlined by the compiler and cannot be modified at runtime. If an attribute is set multiple times within a module, the value used will be the one set closest to the point of usage.

defmodule ModA do
  @name "April"
  def first, do: @name

  @name "O'Neal"
  def last, do: @name
end

# > ModA.first
# "April"
# > ModA.last
# "O'Neal"

TODO: Add details on @external_resource and provide a more in-depth explanation of attributes in relation to metaprogramming.

Documentation with Attributes

Elixir has built-in support for documentation generation. You can document modules and functions using specific attributes:

  • @moduledoc: Describes the module itself.
  • @doc: Describes a module function.
defmodule MathUtils do
  @moduledoc """
  Provides various utility functions for mathematical operations.
  This module is designed to be a helpful resource for common math tasks.
  """

  @doc "Squares the given number."
  def square(n), do: n*n
end

Introspection

Modules in Elixir support introspection, allowing you to query information about them at runtime. A common introspection function is:

  • __info__(:functions): Returns a list of all public functions defined in the module.

For example, MyModule.__info__(:functions) would return information about the functions defined in MyModule.