Modules are a way of grouping functions together in Elixir and as a way of namespacing. Normally, functions that are grouped in a module are related to one another.
We have been using modules such as String and Enum throughout this series so far, and touched on how to define a module in a previous section.
In this section we're going to take a deeper dive into different module features in Elixir.
- Elixir 1.10.0
Define a Module
To define a module we use the defmodule macro, followed by the module name and a do block.
Modules use the CamelCase convention in which each word is capitalized with no spaces or underscores.
Modules must also have unique names.
Module File Naming
File names for Elixir modules also follow the snake_case convention. For example, the Math module should, by convention live in the math.ex file.
You'll likely need to nest some modules inside other namespaces such as with "contexts" in the Phoenix Framework.
In this case, if Accounts is the context and User is the module, you'd expect to have an "accounts" directory with the file "user.ex":
You've probably seen .ex and .exs extensions mentioned in your Elixir journey. The difference is important so I thought it would be good to touch on here.
When executed, both file extensions will compile and load their modules into memory.
Elixir treats both files exactly the same way, the only difference being that .ex files are compiled to bytecode on disk as .beam files. .exs files are not, and therefore more suitable for scripting as interpreted code.
ExUnit tests are a good example of when it makes sense to use .exs files.
Because they're interpreted, you don't have to recompile every time you make a change to your tests.
Module attributes are defined using the @ symbol. Let's use some examples to see how they're used.
They can annotate a module with information about how to use the module:
Here, we've use the reserved attributes in Elixir:@moduledoc and @doc.
Module attributes can also be used as constants:
Notice there is no = operator being used for assignment. At compile time this attribute is replaced by its value.
You can also call a function to assign a module attribute:
But there is something important to keep in mind.
From the docs:
Be careful, however: functions defined in the same module as the attribute itself cannot be called because they have not yet been compiled when the attribute is being defined.
As Temporary Storage
An example of using module attributes as temporary storage can be found in the ExUnit framework which uses them as annotation and storage:
Tags in ExUnit are used to annotate tests which can be later used to filter which tests you'd like to run.
Elixir provides three lexically scoped directives: alias, import and require in addition to a macro use. Let's see how these can be used when working with modules.
All modules in Elixir are compiled to Atoms.
Aliases are used in Elixir to shorten the syntax of referring to other modules.
To see how we've already been using aliases , let's inspect the String module:
The String module is an alias of :"Elixir.String".
Writing String is shorter and easier to remember.
Define an Alias
Let's define an alias of String itself to show how it works:
We can now use Str as an alias of String.
The as: is optional. If it's not provided, Elixir will use the last module. We can see this in Phoenix.
Phoenix organizes code into "contexts" and in default cases has a main Repo module that's responsible for interactions with the database.
Here's an example how aliases are used for an Accounts context:
Because the Repo and User modules will be used several times throughout the Accounts module, the aliases reduce code duplication, improve readability and simplify changes that may be required.
We can alias multiple modules from the same namespace. In our example, if we also needed an alias for MyApp.Accounts.UserIdentity, we could extend the current alias:
Now we can access User and UserIdentity without having to type the full list of namespaces.
NOTE: Multiple aliases are organized alphabetically by convention.
We use the require directive to invoke "macros" from a given module.
(macros are a complex subject which we haven't yet explored, but they're basically extensions of Elixir syntax aka. Syntactic Extensions)
Let's use is_odd/1 as an example:
is_odd/1 is defined as a macro in the Integer module. In order to use it in another module, we'll need to require Integer.
We can use the import directive to access functions from other modules without having to use the fully qualified name.
Whenever a module is "imported" it is also "required".
To import String and use it's functions directly
Limiting Imported Functions
Importing all functions of a module can pollute the module scope and create conflicts with functions in the current module.
We can limit which functions are imported by passing some options to the import directive:
Here we only import the reverse function from String in this format: [function_name: arity]
You can also use exclude: if you want all but certain functions:
use is another macro which requires a given module and calls the __using__ macro defined in that module.
It is commonly used to bring extra functionality from libraries into our modules.
Here's an example of how it's used in a Phoenix Application module:
We're not going to dive too deep into macros as it's complex subject. We're only touching on more basic concepts when working in Elixir.
For now this gives us basic handle on what's happening behind the scenes when we see the use directive in the wild.
This wraps up our "Getting Started with Elixir" series. Thanks for following along!
We've explored many great features of Elixir and as we slowly uncover different parts of Elixir, we can start to see how everything fits together.
This foundation will empower us to confidently build applications using Phoenix.
We'll continue diving into more complex topics as we go, but for now thanks for reading!