Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Modules

modules.livemd

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

Return Home Report An Issue

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.

  1. defmodule a keyword that means “define module”.
  2. 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.
  3. do a keyword that separates the module name and its internal implementation.
  4. 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.

  1. def this means “define function”
  2. do a keyword that separates the function head and the function body.
  3. "hello world" this is the function body. This function returns the string “hello world”.
  4. 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"