Modules and Functions

In Elixir, we organize code in a structured way using modules and functions.

For Elixir beginners we now run into a typical chicken and egg problem. Because how can I show you how Modules and Functions work while you have never used any Elixir code? I try to use code examples which should be easy to understand.

Modules are like containers for functions, which represent reusable pieces of code.

Let’s illustrate this with an example:

iex> defmodule Greetings do (1)
...>   def hello(name) do (2)
...>     "Hello, " <> name <> "!" (3)
...>   end
...> end
{:module, Greetings,
 <<...>>, {:hello, 1}} (4)
iex> Greetings.hello("Alice") (5)
"Hello, Alice!"
1We use the defmodule keyword to define a module. The module’s name always begins with a capital letter.
2We define a function within the module using the def keyword.
3The function concatenates the input name with a greeting message.
4The return value represents the module creation.
5We call the function from outside the module using this syntax.
Both defmodule and def use a do …​ end structure to denote the start and end of the block.
Module names use CamelCase and start with a capital letter, while function names are in snake_case.

As an exercise, let’s save the following module to the file greetings.exs:

defmodule Greetings do
  def hello(name) do
    "Hello, " <> name <> "!"
  end
end

To use the hello/1 function in the Greetings module, we need to load and compile greetings.exs with Code.require_file("greetings.exs") in iex:

$ iex
Erlang/OTP 26 [erts-14.0] [source] [64-bit] [...]

Interactive Elixir (1.15.0) - press Ctrl+C to exit (...)
iex(1)> Code.require_file("greetings.exs")
[
  {Greetings,
   <<70, 79, ...>>}
]
iex(2)> Greetings.hello("Bob")
"Hello, Bob!"

Function Arity

In Elixir, the term "arity" refers to the number of arguments that a function accepts. Functions are identified by both their name and their arity, expressed as name/arity.

The arity concept is essential because it allows us to define multiple functions with the same name but different arities within a single module. Each of these is considered a distinct function due to its unique argument count.

Let’s demonstrate this with a slightly more complex example:

iex> defmodule Greeting do
...>   def greet() do (1)
...>     "Hello, world!"
...>   end
...>
...>   def greet(name) do (2)
...>     "Hello, #{name}!"
...>   end
...>
...>   def greet(name, time_of_day) do (3)
...>     "Good #{time_of_day}, #{name}!"
...>   end
...> end

iex> Greeting.greet()
"Hello, world!"
iex> Greeting.greet("Alice")
"Hello, Alice!"
iex> Greeting.greet("Bob", "morning")
"Good morning, Bob!"

In this example, we’ve defined three versions of the greet function, each with a different arity.

1The greet/0 function, which takes no arguments, returns a generic greeting.
2The greet/1 function accepts one argument and provides a personalized greeting.
3The greet/2 function takes two arguments and offers a personalized greeting that also includes the time of day.

Private Functions

Sometimes, we want to hide certain functions within a module, making them inaccessible from outside. Elixir supports this through private functions, which we declare using the defp keyword:

iex> defmodule SecretMessage do
...>   def public_message(name) do
...>     secret() <> name
...>   end
...>
...>   defp secret do (1)
...>     "Secret Hello, "
...>   end
...> end

iex> SecretMessage.public_message("Alice") (2)
"Secret Hello, Alice"
iex> SecretMessage.secret (3)
** (UndefinedFunctionError) function SecretMessage.secret/0 is undefined or private
    SecretMessage.secret()
1secret/0 is a private function and can only be accessed within its module.
2public_message/1 is public, and can be called from outside its module. It can access secret/0 because they’re in the same module.
3Attempting to call secret/0 from outside its module results in an UndefinedFunctionError because it’s a private function.

Private functions help us hide implementation details and reduce the exposed interface of a module, leading to cleaner and more maintainable code.

Hierarchical Modules

As your projects become more complex, it’s crucial to structure your code into a clear and manageable form. In Elixir, you can achieve this by using hierarchical module names. Hierarchical modules are defined by attaching sub-module names to a parent module using a . delimiter.

Here’s a new example with a Fruit Shop:

iex> defmodule FruitShop.Apples do
...>   def price_per_kg() do
...>     10
...>   end
...> end

iex> FruitShop.Apples.price_per_kg()
10

The . syntax offers a neat shortcut for defining nested modules. Here’s how you can create the same hierarchy using nested module definitions:

iex> defmodule FruitShop do
...>   defmodule Apples do
...>     def price_per_kg() do
...>       10
...>     end
...>   end
...> end

iex> FruitShop.Apples.price_per_kg()
10

Both methods achieve the same result. Your choice between these two depends on your project’s structure and your coding style preference.

Import

The import directive in Elixir provides a way to access public functions from other modules without needing to write out their fully qualified names. This can make your code cleaner and easier to read.

Consider the following FruitShop.Apples module:

iex> defmodule FruitShop.Apples do
...>   def price_per_kg() do
...>     10
...>   end
...> end

By importing this module, you can call its functions directly, without having to prefix them with the module’s name:

iex> import FruitShop.Apples
FruitShop.Apples
iex> price_per_kg()
10

Here, importing FruitShop.Apples lets you call price_per_kg/0 directly, eliminating the need to use the FruitShop.Apples. prefix.

Selective Importing

While importing a module grants you access to all its public functions, there might be times when you want to import only specific functions from a module. Elixir allows you to do this using a selective import.

For instance, suppose the FruitShop.Apples module also had a quantity_in_stock/0 function. But if you only needed price_per_kg/0 in your current context, you could import just that function like so:

iex> defmodule FruitShop.Apples do
...>   def price_per_kg() do
...>     10
...>   end
...>   def quantity_in_stock() do
...>     100
...>   end
...> end

iex> import FruitShop.Apples, only: [price_per_kg: 0]
FruitShop.Apples
iex> price_per_kg()
10

Here, import FruitShop.Apples, only: [price_per_kg: 0] means that only the price_per_kg/0 function from FruitShop.Apples is available for direct calling. This can help reduce naming conflicts and makes it clear which functions are being used from the imported module.

An alternative to only is except, which lets you import all functions except the ones specified.

Alias

The alias directive offers a convenient way to assign a shorter, alternative name to a module. This can improve both readability and maintainability of your code by reducing verbosity when accessing the module’s functions.

Take a look at the FruitShop.Apples module:

iex> defmodule FruitShop.Apples do
...>   def price_per_kg() do
...>     10
...>   end
...> end

To make calling this module’s functions less verbose, you can use the alias directive to assign it a shorter name:

iex> alias FruitShop.Apples, as: Apples
FruitShop.Apples
iex> Apples.price_per_kg()
10

In the code above, we’ve created an alias for FruitShop.Apples as Apples.

For a quicker and more direct way, you can simply use alias FruitShop.Apples. Elixir will automatically infer the alias from the last part of the module name, in this case Apples:

iex> alias FruitShop.Apples
FruitShop.Apples
iex> Apples.price_per_kg()
10

In this example, the alias FruitShop.Apples directive lets you call functions from FruitShop.Apples using the shortened name Apples. This can significantly improve readability when working with modules that have long or complex names.

Use

Elixir’s use keyword is a cornerstone of metaprogramming in Elixir. It is a powerful tool that helps keep our code DRY (Don’t Repeat Yourself) by allowing us to perform certain actions defined in another module within the current module.

I won’t delve too deeply into metaprogramming here. I’m simply covering it to ensure that you can recognize it when you come across it unexpectedly.

Metaprogramming is a technique that helps us write code that generates or modifies other code. In the context of Elixir, we can think of the use keyword as a way to inject code from one module into another. This is accomplished through the use of the using macro in the module that is being used.

Let’s illustrate this with a more comprehensive example involving a Discount module and two fruit modules, Apples and Bananas:

defmodule Discount do
  defmacro __using__(_) do
    quote do
      def apply_discount(price, percentage) do
        price - (price * (percentage / 100))
      end
    end
  end
end

defmodule FruitShop.Apples do
  use Discount

  def price_per_kg() do
    10
  end
end

defmodule FruitShop.Bananas do
  use Discount

  def price_per_kg() do
    5
  end
end

In these examples, both the FruitShop.Apples and FruitShop.Bananas modules use the Discount module. The use keyword triggers the using macro in the Discount module, which in turn injects the apply_discount/2 function definition into the FruitShop.Apples and FruitShop.Bananas modules. Therefore, we can call apply_discount/2 directly on either of these modules:

iex> FruitShop.Apples.apply_discount(10, 20)
8

iex> FruitShop.Bananas.apply_discount(5, 15)
4.25

In these cases, we’ve applied a 20% discount to the price of apples (which was 10), and the result is 8. Similarly, we’ve applied a 15% discount to the price of bananas (which was 5), and the result is 4.25.

By leveraging the power of the use keyword and metaprogramming, we’ve written a versatile Discount module that can be used across multiple fruit modules to apply discounts to their prices.

If you’re working with a Phoenix application, you might see use ExUnit.Case in your test files. This is a practical example where ExUnit.Case provides a set of functionalities (like assert functions) that will be accessible within your test cases.