Exploring Elixir’s Advanced Features: Part 5 of the Elixir Series

Welcome to the fifth part of our Elixir series, where we dive deeper into the advanced features and concepts that make Elixir a powerful and expressive language. In this part, we’ll explore modules, named functions, guard clauses, default parameters, private functions, the pipe operator, module creation and usage, attributes, module names, and more.

Structuring Code with Modules and Named Functions

Elixir encourages structured code organization by breaking it into named functions grouped within modules. In Elixir, named functions must reside inside modules. Function names are identified by their arity, indicating the number of parameters they accept. For example, a function named double/1 accepts one parameter. If we had another version of double that took three parameters, it would be known as double/3. These functions are entirely separate in Elixir’s eyes.

defmodule Times do
  def double(n) do
    n * 2
  end
end

Module Compilation

1. Compilation with .ex Files

.ex files are used for production code. These files contain compiled bytecode and are typically part of your project's source code.

Here’s how you can compile a module from an .ex file:

$ elixirc my_module.ex

This command compiles the module and creates a corresponding .beam file. You can then use this compiled module in your Elixir project.

2. Scripting and Interactive Development with .exs Files

.exs files, on the other hand, are intended for scripting, testing, and interactive development. These files do not produce compiled bytecode and are useful for quick experimentation and scripting tasks.

Here’s how you can run an .exs script:

$ elixir my_script.exs

When using .exs files, there's no need for explicit compilation. Elixir interprets the code directly from the script.

Remember that if you want to use a module as part of your production application, it’s best to use .ex files. However, for quick scripts and interactive development, .exs files are more convenient.

These two file types provide flexibility in how you work with Elixir, allowing you to choose the best approach for your specific use case.

The Function’s Body Is a Block

Elixir uses a do...end block to group expressions and pass them to other code. This construct is used in module and named function definitions, control structures, and anywhere code needs to be treated as an entity. The actual syntax for a function's body is as follows:

def double(n), do: n * 2

You can pass multiple lines to do: by grouping them with parentheses.

Multiple Clauses in Named Functions

Elixir allows named functions to have multiple clauses, each with specific patterns. When you call a named function, Elixir attempts to match your arguments with the parameter list of the first definition (clause). If there is no match, it tries the next definition of the same function, provided it has the same arity, continuing until it runs out of candidates.

Here’s an example with the factorial function:

defmodule Factorial do
  def of(0), do: 1
  def of(n), do: n * of(n-1)
end

In this case, there are two definitions of the same function, and Elixir selects the appropriate one based on the provided argument.

One important point to note is that the order of function clauses can make a difference. Elixir tries functions from the top down, executing the first match.

defmodule BadFactorial do
  def of(n), do: n * of(n-1)
  def of(0), do: 1
end

In this example, the order of the function clauses in the BadFactorial module matters. Elixir will try to match the clauses from top to bottom, executing the first one that matches. In this case, the first clause (def of(n), do: n * of(n-1)) will always match because it doesn't have any guard clause, resulting in an infinite loop when trying to calculate the factorial. The second clause (def of(0), do: 1) will never be reached.

This demonstrates how the order of function clauses can impact the behavior of your code in Elixir.

Guard Clauses

Guard clauses are predicates attached to function definitions using the when keyword. They provide additional conditions for pattern matching. Here's an example:

defmodule Guard do
  def what_is(x) when is_number(x) do
    IO.puts "#{x} is a number"
  end
  def what_is(x) when is_list(x) do
    IO.puts "#{inspect(x)} is a list"
  end
end

Guard clauses allow you to specify additional conditions for a function’s clauses, enabling more fine-grained pattern matching.

Default Parameters

Elixir allows you to provide default values for function parameters. You use the param \\ value syntax to specify defaults. If a parameter isn't provided when calling the function, it defaults to the specified value. However, when defining multiple heads of a function with default parameters, there's a specific syntax:

defmodule DefaultParams1 do
  def func(p1, p2 \\ 123) do
    IO.inspect [p1, p2]
  end
  def func(p1, 99) do
    IO.puts "you said 99"
  end
end

If you compile this module, you may encounter a warning:

warning: definitions with multiple clauses and default values require a function head. Instead of this:
    def func(:first_clause, b \\ :default) do ... end
    def func(:second_clause, b) do ... end
one should write this:
    def func(a, b \\ :default)
    def func(:first_clause, b) do ... end
    def func(:second_clause, b) do ... end

The intent is to reduce confusion that can arise with defaults. Adding a function head with no body that contains the default parameters ensures the defaults apply to all calls to the function.

Example:

defmodule Params do
  def func(p1, p2 \\ 123)
  def func(p1, p2) when is_list(p1) do
    "You said #{p2} with a list"
  end
  def func(p1, p2) do
    "You passed in #{p1} and #{p2}"
  end
end

Private Functions

Elixir allows you to define private functions using the defp macro. Private functions are only accessible within the module that declares them, providing encapsulation and hiding internal implementation details. You can define private functions with multiple heads just like public functions. However, you cannot have some heads as private and others as public.

Example:

defmodule MyModule do
  def my_function() do
    some_public_function()
    my_private_function()
  end
  defp my_private_function() do
    # Private logic
  end
  def some_public_function() do
    # Public logic
  end
end

Private functions are essential for maintaining code organization and protecting internal details from external code.

The Amazing Pipe Operator: |>

Elixir’s pipe operator, |>, simplifies code composition. It allows you to chain function calls, making your code more readable and expressive. Consider the following example:

filing = DB.find_customers
         |> Orders.for_customers
         |> sales_tax(2018)
         |> prepare_filing

The pipe operator passes the result of each expression to the next function, enhancing code clarity and reducing nesting.

Module Nesting

In Elixir, module nesting is a way to organize code logically, but it’s an organizational feature rather than a strict hierarchy. When you define a module inside another, Elixir simply prepends the outer module name to the inner module name, separating them with a dot. This approach allows you to create nested modules directly.

defmodule Mix.Tasks.Doctest do
  def run do
    # Implementation
  end
end

Module nesting provides a convenient way to structure your code, but it’s important to note that there’s no inherent relationship between the outer and inner modules.

Importing Modules

The import directive in Elixir allows you to bring a module's functions and macros into the current scope. This feature reduces clutter in your source files and eliminates the need to repeat the module name. Here's an example of how to use it:

defmodule Example do
  def func1 do
    List.flatten([1, [2, 3], 4])
  end
  def func2 do
    import List, only: [flatten: 1]
    flatten([5, [6, 7], 8])
  end
end

By importing the List module, you can call its functions without specifying the module name repeatedly.

The Alias Directive

Elixir’s alias directive creates an alias for a module, which is particularly useful for reducing typing effort. You can create aliases for modules to make your code more concise and readable.

defmodule Example do
  def compile_and_go(source) do
    alias My.Other.Module.Parser, as: Parser
    alias My.Other.Module.Runner, as: Runner
    source
    |> Parser.parse()
    |> Runner.execute()
  end
end

Aliases enhance code readability and minimize the need for lengthy module references.

The Require Directive

In Elixir, you use the require directive to include modules that define macros. This ensures that macro definitions are available during code compilation, enabling the use of macros in your code. The require directive is particularly relevant when working with macros.

Module Attributes

Elixir modules can have attributes, which are metadata associated with the module and identified by a name. These attributes are accessed using the @ symbol within a module. Attributes are often used for configuration and metadata purposes.

Example:

defmodule Example do
  @author "Dave Thomas"
  def get_author do
    @author
  end
end

Attributes can be set multiple times within a module, and the value you see in a named function is based on the attribute’s definition at the time the function is defined.

Here’s an example demonstrating that attributes can be set multiple times within a module, and the value you see in a named function is based on the attribute’s definition at the time the function is defined:

defmodule Example do
  @attr "one"

  def first, do: @attr

  @attr "two"
  def second, do: @attr
end
IO.puts "#{Example.second} #{Example.first}"

In this example, we define the @attr attribute twice within the Example module, first with the value "one" and later with the value "two." The two named functions, first and second, each access the @attr attribute. When we print the values of Example.second and Example.first, you'll see that the attribute's value is based on the definition at the time the function is defined. In this case, it will output "two one."

You now have a comprehensive guide to the advanced features of Elixir, including module compilation differences, function clauses, default parameters, private functions, module attributes, and more. These features are essential for writing efficient, well-structured, and expressive code in Elixir.

Did you find this article valuable?

Support Ayoush Chourasia by becoming a sponsor. Any amount is appreciated!