Powered by AppSignal & Oban Pro

Modules and functions

modules-and-functions.livemd

Modules and functions

In Elixir, we group functions in modules. We’ve already used many different modules in the previous chapters, such as the String module:

String.length("hello")

In order to create our own modules in Elixir, we use the defmodule macro. The convention is to start module name with an uppercase letter, but it can also be any atom. To define functions in the module, we can use def macro. A function name should start with a lowercase letter or an underscore.

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

Math.sum(10, 11)

Inside a module, we can also define private functions with defp/2. A function defined with def/2 can be invoked from other modules while a private function can only be invoked locally.

defmodule Math2 do
  def sum(a, b) do
    do_sum(a, b)
  end

  defp do_sum(a, b) do
    a + b
  end
end

Math2.sum(11, 12)
# 💡 Try calling Math2.do_sum from outside of the module

Function declarations also support guards and multiple clauses. If a function has several clauses, Elixir will try each clause until it finds one that matches. Here is an implementation of a function that checks if the given integer is zero or not:

defmodule Math3 do
  def zero?(0) do
    true
  end

  def zero?(x) when is_integer(x) do
    false
  end
end

Math3.zero?(0)
# 💡 Try calling Math3.zero? with an integer other that 0
# 💡 Try calling Math3.zero? with a float

The trailing question mark in zero? is just a part of the function name and doesn’t have any special implications. However, the convention is to use it for functions that return a boolean. To learn more about the naming conventions for modules, function names, variables and more in Elixir, see Naming Conventions.

Default arguments

Function definitions in Elixir also support default arguments:

defmodule Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end

Concat.join("hello", "world", "_")
# 💡 Try calling Concat.join with two arguments

Note that the expression provided as a default argument is evaluated on each function call. To see it, try running the below snippet a few times:

defmodule Concat2 do
  def join(a, b, sep \\ Enum.random([" ", "_", ", ", "-"])) do
    a <> sep <> b
  end
end

Concat2.join("hello", "world")

If a function with default values has multiple clauses, it is required to create a function head (a function definition without a body) for declaring defaults:

defmodule Concat3 do
  # A function head declaring defaults
  def join(a, b, sep \\ " ")

  def join(a, b, _sep) when b == "" do
    a
  end

  def join(a, b, sep) do
    a <> sep <> b
  end
end

Concat3.join("hello", "")

Module namespacing & nesting

For now, we’ve been using single words as module names. In real projects, module names are usually prefixed with the project name and possibly internal namespaces, using a dot . separator:

defmodule MyProject.Utils.Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end

MyProject.Utils.Concat.join("hello", "world")

Actually, all modules are defined under Elixir namespace, but for convenience you can omit Elixir.

💡 Try prefixing MyProject.Utils.Concat.join(...) above with Elixir. - does it still work?

Modules can also be nested within one another. The following snippet creates two modules: Foo and Foo.Bar.

defmodule Foo do
  defmodule Bar do
    def hello do
      :ok
    end
  end
end

Foo.Bar.hello()

When modules are nested, the outer module can access the inner one by an unprefixed name:

defmodule Foo2 do
  defmodule Bar do
    def hi do
      "hi"
    end
  end

  def hello() do
    Bar.hi()
  end
end

Foo2.hello()

In real-world projects, it’s generally better to avoid nesting modules, especially when the inner module is used outside of the outer module. The usual approach is to have one module per file.