Modules
Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"}
])
Navigation
Home Report An Issue Rock Paper ScissorsStructsReview Questions
Upon completing this lesson, a student should be able to answer the following questions.
- How do you define a module?
- How do you use public vs private named functions?
- What is scope and how does it relate to modules?
- What is function arity?
- How do you read module documentation and run doctests?
Modules
As you create more and more functions, it becomes necessary to organize them. That’s just one of the many reasons to use a module. A module is more or less a “bag of functions”. We use them to organize and group related functions together.
We use the defmodule
keyword to define a module like so.
defmodule MyModule do
end
Don’t worry about the output {:module, MyModule, <<70, 79, 82, 49, 0, 0, 4, ...>>, nil}
. That’s just how Elixir represents modules internally, and it’s not important for our understanding.
Let’s break down the code above.
-
defmodule
a keyword that means “define module”. -
MyModule
is the name of this module. It can be any valid name, and should be CapitalCase which is often called PascalCase. you’ll often hear the name of the module referred to as the namespace that functions are organized under. -
do
a keyword that separates the module name and its internal implementation. -
end
a keyword that finishes the module definition.
Modules define functions inside of them. Each function has a name, so they are called named functions. You can define functions inside a module using the following syntax.
defmodule ModuleName do
def function_name do
"return value"
end
end
Let’s break down the named function above.
-
def
this means “define function”. The function name in this example isfunction_name
but it could be any valid function name. -
do
a keyword that separates the function head and the function body. -
"return value"
this is the function body. This function returns the string"return value"
, but it could return any Elixir term. -
end
is a keyword that ends the function definition.
Calling Named Functions
To call a named function, you use round brackets ()
.
ModuleName.function_name()
Arguments And Parameters
You can provide arguments to a function when you call it. These arguments are bound to parameters in the function definition.
defmodule ArgsAndParamsExample do
def call(param) do
IO.inspect(param, label: "Called with the following argument")
end
end
ArgsAndParamsExample.call("argument")
Multiple Arguments And Params
You can define multiple parameters in a function, each separated by a comma. The names of parameters follow the same naming rules as variables.
We can use binding()
to get a keyword list of all bound values. We commonly use this to conveniently inspect our code.
defmodule MultiParamExample do
def call(param1, param2, param3) do
IO.inspect(binding(), label: "Called with the following bindings")
end
end
Your Turn
Call the MutliParamExample.call/3
function with different arguments.
Internal Module Functions
A module can use its own functions without using the module’s name before the function.
defmodule InternalExample do
def internal do
IO.inspect("Internal was called")
end
def public do
InternalExample.internal()
internal()
"public finished executing"
end
end
InternalExample.public()
The world outside of the module must use the module name to know which function to execute.
InternalExample.public()
The module name is optional inside of the module. However, we must include the module name when using the module’s function outside of the module. For example, the following will raise an error.
public()
Private Functions
Modules can access other module functions.
defmodule Speaker do
def speak() do
"hi there"
end
end
defmodule Listener do
def listen() do
"I heard you say: " <> Speaker.speak()
end
end
Listener.listen()
However, sometimes a module must keep a function private for internal use only. It may be for security reasons or because you don’t think the function should be used anywhere but internally. Often it communicates to other developers how to use your module.
You can create a private module function with defp
instead of def
. You’ll notice that below the Speaker.think/0
function is undefined to the outside world.
defmodule PrivateThoughts do
defp think() do
"I really want to say hi..."
end
end
PrivateThoughts.think()
We use private functions internally in the module, which means that public functions could expose their values.
defmodule ThoughtfulSpeaker do
defp think() do
IO.inspect("I really want to say hi...")
end
def speak() do
think()
"hi there"
end
end
ThoughtfulSpeaker.speak()
Callback Functions
Similar to anonymous functions, we can pass named functions as callback functions, however we have to explicitly provide the function’s arity using the capture operator &
.
defmodule HigherOrder do
def higher_order_function(callback) do
callback.()
end
end
defmodule Callback do
def callback_function do
"I was called!"
end
end
HigherOrder.higher_order_function(&Callback.callback_function/0)
Notice we cannot simply pass Callback.callback_function
as an argument.
HigherOrder.higher_order_function(Callback.callback_function())
Alternatively, we can wrap the callback function in an anonymous function.
HigherOrder.higher_order_function(fn -> Callback.callback_function() end)
Namespaces
You can use modules to organize functions under a single namespace. This allows you to create many unique namespaces with their own functions to organize the functionality of your program.
flowchart
A[Namespace]
B[Namespace]
C[Namespace]
A1[Function]
A2[Function]
A3[Function]
B1[Function]
B2[Function]
B3[Function]
C1[Function]
C2[Function]
C3[Function]
A --> A1
A --> A2
A --> A3
B --> B1
B --> B2
B --> B3
C --> C1
C --> C2
C --> C3
SubModules
Sometimes you need to further split the functions in a module. This can be because the module is too large, or because the module has multiple separate responsibilities and it’s more clear to separate them.
flowchart
Module --> SubModule
SubModule --> a[Function]
SubModule --> b[Function]
SubModule --> c[Function]
To create submodules, you can separate module names with a period .
.
defmodule Languages.English do
def greeting do
"Hello"
end
end
Languages.English.greeting()
Your Turn
Create a submodule under the Languages
module with a greeting/0
function that returns a greeting in another language. You may choose the language and the name of the submodule.
Example solution
defmodule Languages.Spanish do
def greeting do
"Hola"
end
end
Enter your solution below.
Nested Modules
It’s also possible, though not very common, to define a module inside another module. This will automatically nest the namespaces.
defmodule NestedLanguages do
defmodule English do
def greeting do
"Hello"
end
end
defmodule Spanish do
def greeting do
"Hola"
end
end
end
NestedLanguages.English.greeting()
NestedLanguages.Spanish.greeting()
Module Attributes
While modules are mostly used to group functions, we can also include compile-time data inside the module that our functions can all use.
We use the @
symbol to define a compile-time module attribute.
defmodule Hero do
@name "Spider-Man"
@nemesis "Green Goblin"
def introduce do
"Hello, my name is #{@name}!"
end
def catchphrase do
"I'm your friendly neighborhood #{@name}!"
end
def defeat do
"I #{@name} will defeat you #{@nemesis}!"
end
end
Hero.introduce()
Hero.catchphrase()
Hero.defeat()
We can use module attributes to avoid significant code repetition where many functions all need to use the same value.
Your Turn
Change the module attributes for the Hero
module above to match your favourite super hero and villain. Re-evaluate the Hero
module, then re-evaluate each function call to see the output change.
Module Scope
Modules and functions close themselves to the outside world. We call this scope
.
Modules, functions, and many other similar constructs in Elixir are lexically scoped.
That means that variables defined in one scope cannot be accessed in another scope.
flowchart
subgraph Top Level Scope
A[top level variable]
subgraph Module Scope
B[module variable]
subgraph Function Scope
C[function variable]
end
end
end
Notice how the following example has an error because we cannot access the variable
top_level_scope
.
top_level_scope = 1
defmodule ModuleScope1 do
def function_scope do
top_level_scope
end
end
The same is true for the module scope. However, it’s uncommon to bind variables in the module scope. Generally we use module attributes instead.
defmodule ModuleScope2 do
module_scope = 2
def function_scope do
module_scope
end
end
To use a variable, it must be bound in the same scope.
top_level_scope = "top level scope"
IO.inspect(top_level_scope)
defmodule CorrectModuleScope do
module_scope = "module scope"
IO.inspect(module_scope)
def function_scope do
function_scope = "function scope"
IO.inspect(function_scope)
end
end
CorrectModuleScope.function_scope()
Multiple Function Clauses
Elixir allows us to define multiple functions with the same name but that expect different parameters, either with different arity or different values.
defmodule MultipleFunctionClauses do
def my_function do
"arity is 0"
end
def my_function(_param1) do
"arity is 1"
end
def my_function(_param1, _param2) do
"arity is 2"
end
end
MultipleFunctionClauses.my_function(1)
Each function clause has a different arity. We can treat each function with a different arity as a completely separate function. In Elixir, we often refer to functions by their name and arity.
So above we define MultipleFunctionClauses.my_function/0
, MultipleFunctionClauses.my_function/1
, and MultipleFunctionClauses.my_function/2
functions.
Your Turn
Create a Math
module with add/2
and add/3
functions. Each should add all of its parameters
together.
Math.add(2, 5) # 7
Math.add(1, 2, 3) # 6
Example solution
defmodule Math do
def add(int1, int2, int3) do
int1 + int2 + int3
end
def add(int1, int2) do
int1 + int2
end
end
Default Arguments
You can provide default arguments to functions using the \\
syntax after the parameter
and then the default value.
defmodule DefaultArgsExample do
def call(param \\ "default argument") do
param
end
end
DefaultArgsExample.call()
If desired, you can override the default value.
DefaultArgsExample.call("overridden argument")
Multiple parameters can have default values.
defmodule MultipleDefaultArgs do
def all_defaults(param1 \\ "default argument 1", param2 \\ "default argument 2") do
binding()
end
def some_defaults(param1 \\ "default arg 1", param2) do
binding()
end
end
MultipleDefaultArgs.all_defaults()
We can even provide default arguments for only some parameters in the function.
MultipleDefaultArgs.some_defaults("overridden argument")
This sometimes results in some confusion with the function’s arity, and the number of arguments passed into the function. For example, the MultipleDefaultArgs.some_defaults/2
function above has an arity of 2
, despite only being called with one argument.
Your Turn
In the Elixir cell below, define a module Greeting
module with a hello/1
function that uses a default argument. The hello/1
function should return Hello name!
where name
is some default value that you choose.
Greeting.hello()
"Hello Jon!"
Greeting.hello("Andrew")
"Hello Andrew!"
Documentation
We can document modules using @doc
and @moduledoc
module attributes with a multiline string.
@moduledoc
should describe the module at a high level. @doc
should document a single function in the module.
defmodule DocumentedModule do
@moduledoc """
Documented Module
Explanation of the module
"""
@doc """
Explanation of the documented function 1
"""
def documented_function1 do
end
@doc """
Explanation of the documented function 1
"""
def documented_function2 do
end
end
Doctests
Often documentation contains examples of how the code should behave. Livebook automatically runs these examples to ensure the module is working correctly.
By starting a line with iex>
in the documentation, we declare that the code example should be executable. This represents the IEx Shell which we will learn more about in a future lesson.
We often use these doctests to provide test feedback for exercises. You’ll want to get comfortable reading this feedback.
defmodule DoctestExample do
@moduledoc """
DoctestExample
Explains doctests with examples
"""
@doc """
Returns a personalized greeting for one person.
## Examples
iex> DoctestExample.hello()
"Hello Jon!"
iex> DoctestExample.hello("Bill")
"Hello Bill!"
"""
def hello(name \\ "Jon") do
"Hello #{name}!"
end
end
Failing Doctests
If the doctests fail, you’ll see a failure message and explanation of why.
defmodule DoctestFailure do
@moduledoc """
DoctestFailure
Explains doctest failures with examples
"""
@doc """
Purposely fails doctest
## Examples
iex> DoctestFailure.test()
"expected return value"
"""
def test do
"actual return value"
end
end
The result displays information about where the test failed, and what the expected vs actual result of the function was.
You can use this feedback to debug your exercises.
Your Turn
Fix the failing DoctestFailure
module so that its doctests pass.
Crashing Doctests
Unfortunately, if the doctest causes code to crash, it usually won’t display nicely formatted test results. Instead, it will either crash Livebook, or cause tests not to run.
If this is the case, you either have to fix your code, or remove/comment the crashing doctest.
Your Turn
Uncomment the following code and evaluate the cell to see an example. Re-comment your code to avoid crashing the livebook as you continue with the lesson.
# defmodule DoctestCrash Do
# @moduledoc """
# DoctestCrash
# Explains Doctest Crashes With Examples
# """
# @doc """
# Purposely Crashes Doctest Using Invalid Pattern Matching
# ## Examples
# iex> {} = DoctestCrash.crash()
# "2"
# """
# def crash do
# end
# end
Your Turn
Use @doc
and @moduledoc
to document the Math
module you previously created in this lesson.
Commit Your Progress
DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.
Run git status
to ensure there are no undesirable changes.
Then run the following in your command line from the curriculum
folder to commit your progress.
$ git add .
$ git commit -m "finish Modules reading"
$ git push
We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.
We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.