Modules
Mix.install([
{:kino, github: "livebook-dev/kino", override: true},
{:kino_lab, "~> 0.1.0-dev", github: "jonatanklosko/kino_lab"},
{:vega_lite, "~> 0.1.4"},
{:kino_vega_lite, "~> 0.1.1"},
{:benchee, "~> 0.1"},
{:ecto, "~> 3.7"},
{:math, "~> 0.7.0"},
{:faker, "~> 0.17.0"},
{:utils, path: "#{__DIR__}/../utils"}
])
Navigation
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”.
Here’s what an empty module looks like.
defmodule Greeter do
end
Again, don’t worry about the somewhat strange-looking return value.
{:module, Greeter, <<70, 79, 82, 49, 0, 0, 4, ...>>, nil}
That’s how Elixir represents
modules internally.
Let’s break down what this all means.
-
defmodule
a keyword that means “define module”. -
Greeter
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 Greeter do
def hello do
"hello world"
end
end
Let’s break down the named function above.
-
def
this means “define function” -
do
a keyword that separates the function head and the function body. -
"hello world"
this is the function body. This function returns the string “hello world”. -
end
is a keyword that ends the function definition.
Calling A Named Function
To call a function inside a module, you use Module.function(arguments)
syntax.
Greeter.hello()
Named Functions With Parameters
You can create multiple functions in a module.
Here’s a new hi/1
function that says hi to a person.
defmodule Greeter do
def hello do
"hello world"
end
def hi(name) do
"hi #{name}"
end
end
You call the named function in the module by passing it an argument.
Greeter.hi("Peter Parker")
Your Turn
In the Elixir cell below, create a Math
module with a function add
that adds two integers
together.
Internal Module Functions
A module can use its own functions.
defmodule InspectorGadget do
def gogo(gadget) do
"Go go gadget #{gadget}!"
end
def necktie do
InspectorGadget.gogo("Necktie")
end
end
InspectorGadget.necktie()
However, you can omit the module name. notice gogo
instead of InspectorGadget.gogo
defmodule InspectorGadget do
def gogo(gadget) do
"Go go gadget #{gadget}!"
end
def necktie do
gogo("necktie")
end
end
InspectorGadget.necktie()
Your Turn
In the Elixir cell below, add an arms/0
function to the InspectorGadget
module
that calls gogo("arms")
defmodule InspectorGadget do
def gogo(gadget) do
"Go go gadget #{gadget}!"
end
end
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. Why? 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 Speaker do
defp think() do
"hi there"
end
end
Speaker.think()
We use private functions internally in the module, which means that public functions could expose their values.
defmodule Speaker do
defp think() do
"hi there"
end
def speak() do
think()
end
end
Speaker.speak()
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
However, 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 do this you can nest modules with a period .
.
defmodule MyModule.SubModule do
def my_function do
"my return value"
end
end
MyModule.SubModule.my_function()
Your Turn
In the Elixir cell below, create two submodules under as single namespace.
Module Attributes
What if you have many functions in a module that all use the same value? You’ve already learned that repeating the same hard-coded value over and over again isn’t very reusable, and you’ve used variables to pass the same value around in your code. However, that’s not possible in a module.
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 MyModule do
def my_function do
top_level_scope
end
end
The same is true for the module scope.
defmodule MyModule do
module_scope = 2
def my_function do
module_scope
end
end
To get around this, Elixir allows you to store constant values as module attributes
using the @module_attribute value
syntax. You can then access the @module_attribute
value in
any module function.
flowchart
direction TB
subgraph A[Top Level Scope]
a[Top Level Variable] --- b
direction TB
subgraph B[Module Level Scope]
b[Define Module Attribute] --- c
subgraph C[Function Level Scope]
c[Access Module Attribute]
end
end
end
Notice that we can use a module attribute inside of the module’s function now.
defmodule MyModule do
@my_attribute "any valid data type"
def my_function do
@my_attribute
end
end
MyModule.my_function()
We can also access a variable in the top-level scope as long as we set a module attribute.
top_level_variable = 1
defmodule MyModule do
@my_attribute top_level_variable
def my_function do
@my_attribute
end
end
MyModule.my_function()
Now we can easily share constant values between multiple module functions.
defmodule Hero do
@name "Batman"
@nemesis "Joker"
def catchphrase do
"I am #{@name}."
end
def victory do
"I #{@name} will defeat you #{@nemesis}!"
end
end
Hero.victory()
If the module attribute value changes, you only need to change the module attribute.
defmodule Hero do
@name "Iron Man"
@nemesis "Iron Monger"
def catchphrase do
"I am #{@name}."
end
def victory do
"I #{@name} will defeat you #{@nemesis}!"
end
end
Hero.victory()
Multiple Function Clauses
Elixir allows us to define multiple functions with the same name but that expect different parameters.
This means the function has multiple function clauses.
For example, we can take our hi
function and create a new version that says hi to two people.
defmodule Greeter do
def hi(name1, name2) do
"hi #{name1}, hi #{name2}"
end
def hi(name) do
"hi #{name}"
end
end
Greeter.hi("Peter Parker", "Mary Jane")
In the example above, each function has a different arity. You can treat each function with
a different arity as a different function. We often refer to each function by its arity
as function/arity
.
So the Greeter
module has a hi/1
(hi one) function and hi/2
(hi two) function.
Your Turn
Create a Math
module with add/2
and add/3
functions. Each should add all of its parameters
together.
Default Arguments
You can provide default arguments to functions using the \\
syntax after the parameter
and then the default value.
defmodule Greeter do
def greet(name, greeting \\ "Hello") do
"#{greeting} #{name}!"
end
end
Greeter.greet("Peter")
Then if desired, you can override the default value.
defmodule Greeter do
def greet(name, greeting \\ "Hello") do
"#{greeting} #{name}!"
end
end
Greeter.greet("Peter", "Hi")
Multiple parameters can have default values.
defmodule Greeter do
def greet(name \\ "Peter", greeting \\ "Hello") do
"#{greeting} #{name}!"
end
end
Greeter.greet()
However, you can even have a default for the first of multiple parameters. Elixir is smart enough to handle that!
defmodule Greeter do
def greet(name \\ "Peter", greeting) do
"#{greeting} #{name}!"
end
end
Greeter.greet("HI")
Your Turn
In the Elixir cell below, define a module with a function that uses a default argument.
Commit Your Progress
Run the following in your command line from the project folder to track and save your progress in a Git commit.
$ git add .
$ git commit -m "finish modules section"