Mix.install([
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"},
{:tested_cell, github: "brooklinjazz/tested_cell"},
{:utils, path: "#{__DIR__}/../utils"}
])
Ensure you type the ea
keyboard shortcut to evaluate all Elixir cells before starting. Alternatively, you can evaluate the Elixir cells as you read.
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 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.
To call a function inside a module, you use Module.function(arguments)
syntax.
Greeter.hello()
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"
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")
Create a Math
module with an add/2
function that adds two numbers together.
Example solution
defmodule Math do
def add(int1, int2) do
int1 + int2
end
end
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()
The module can omit the module name when calling an internal function. Below, we call gogo/1
instead of InspectorGadget.gogo/1
defmodule InspectorGadget do
def gogo(gadget) do
"Go go gadget #{gadget}!"
end
def necktie do
gogo("necktie")
end
end
InspectorGadget.necktie()
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.
gogo("necktie")
Add an arms/0
function to the InspectorGadget
module above that calls gogo("arms")
.
Example solution
defmodule InspectorGadget do
def gogo(gadget) do
"Go go gadget #{gadget}!"
end
def necktie do
gogo("necktie")
end
def arms do
gogo("arms")
end
end
InspectorGadget.arms()
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 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()
Similar to anonymous functions, we can pass named functions as callback functions, however we have to explicitly provide the functions 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)
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
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()
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.
It's also possible, though not very common, to define a module inside another module. This will automatically nest the namespaces.
defmodule Languages do
defmodule English do
def greeting do
"Hello"
end
end
defmodule Spanish do
def greeting do
"Hola"
end
end
end
Languages.English.greeting()
Languages.Spanish.greeting()
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.
Change the module attributes for the Hero
module above to match your favourite super hero. Re-evaluate the Hero
functions to see the output change.
You will have to re-evaluate the Hero
module after changing it, to ensure it compiles with the latest version.
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
We've already seen we can share a value between functions in a module using module attributes. We can also use module attributes to provide access to outside variables. This isn't very common, and should not be overused, but it's a useful trick to be aware of.
name = "Jon"
defmodule Greeter do
@name name
def hello do
"Hello #{@name}!"
end
end
Greeter.hello()
Elixir allows us to define multiple functions with the same name but that expect different parameters.
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")
Each function clause has a different arity. We can treat each function with a different arity as a completely separate function.
So above we have a Greeter.hi/1
function, and a Greeter.hi/2
function.
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
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()
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")
This sometimes results in some confusion with the function's arity, and the number of arguments passed into the function. For example, the Greeter.greet/2
function above has an arity of 2
, despite only being called with fewer than two arguments.
In the Elixir cell below, define a module with a function that uses a default argument.
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 Greeter do
@moduledoc """
Greeter
Return common greetings.
"""
@doc """
Returns a personalized greeting for one person.
"""
def greet(name) do
"Hello #{name}!"
end
@doc """
Returns a personalized greeting for two people.
"""
def greet(name1, name2) do
"Hello #{name1}! Hello #{name2}!"
end
end
Sometimes documentation will include examples of how the code should behave. This is usually kept in an examples section.
We use iex>
to declare that the code example should be executable. This represents the IEx Shell which we will learn more about in a future lesson.
defmodule Greeter do
@moduledoc """
Greeter
Return common greetings.
"""
@doc """
Returns a personalized greeting for one person.
## Examples
iex> Greeter.greet("Bill")
"Hello Bill!"
"""
def greet(name) do
"Hello #{name}!"
end
@doc """
Returns a personalized greeting for two people.
## Examples
iex> Greeter.greet("Bill", "Jon")
"Hello Bill! Hello Jon!"
"""
def greet(name1, name2) do
"Hello #{name1}! Hello #{name2}!"
end
end
Livebook automatically executes these examples to ensure they work as documented. You will start to see these used in course exercises.
Use @doc
and @moduledoc
to document the Math
module you previously created in this lesson.
Run the following in your command line from the beta_curriculum folder to track and save your progress in a Git commit.
$ git add .
$ git commit -m "finish modules section"
Previous | Next |
---|---|
Rock Paper Scissors | Structs |