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