elixir, functions

Getting Started with Elixir - Functions

In Elixir, functions are first-class citizens.  This means the Elixir supports   assigning functions to variables, passing them as arguments to other functions and returning them as values from other functions. 

Ensure you have Elixir installed so you can experiment in IEx:

$ iex -S mix

Versions

  • Elixir 1.10.0

Elixir Series

Anonymous Functions

Elixir supports anonymous functions, sometimes called lambdas.

They're created using the fn keyword: 

iex> add_one = fn (x) -> x + 1 end

iex> add_one.(1)
2

We're defining an anonymous function that accepts one argument x and assigning the function to the variable add_one.

NOTE: The parenthesis are optional: fn x -> x + 1 end would also work.

Capture Operator & 

We can also use Elixir's & shorthand notation known as the capture operator to define the same function:

iex> add_one = &(&1 + 1)

iex> add_one.(5)
6

With this syntax, & will define the anonymous function, and &1 represents the functions first argument. &n will represent the nth argument.

Similar to the fn notation, the parenthesis are optional but it's better to use them for readability.

Multiple Implementations

We can also use Pattern Matching (covered in the previous section) to define multiple implementations of a function depending on the argument values.

A division operation is a good example for this:

iex> divide = fn
...> (_a, 0) -> :infinity
...> (a, b) -> a / b
...> end

iex> divide.(2, 0)
:infinity

iex> divide.(4, 2)
2.0

The first execution pattern matches against 0 as the second argument and returns :infinity.  

The second execution uses the second implementation to divide the numbers.  (Reminder: division always returns a float in Elixir).

Lexical Scope

We're not going to go into the details of Lexical Scope, but I just wanted to mention that Anonymous Functions can access variables from assigned in the "outer scope".

iex> name = "Titus"

iex> dog_name = fn -> "Dog's name is #{name}" end

iex> dog_name.()
"Dog's name is Titus"

Here our function accesses name from the outer scope.  The new value can be bound to name, but our function will still hold a reference to name's value when our function was defined.

iex> name = "Spike"
"Spike"
iex> dog_name.()
"Dog's name is Titus"

This is usually called a Closure: the function captures the memory locations of all variables used within it.

Named Functions

Much like variable names, function names use the snake case format.  They may also end with ! or ?.   The convention in Elixir is functions ending with ! denote that the function may raise an error and functions ending with ? will return a boolean value (true or false).

Defining Modules

Modules have been described as "containers" for functions.

Because named functions can't be defined outside modules, we're going to briefly touch on how to define a module:

iex> defmodule Helper do
end
{:module, Helper, ... }

A module is defined used defmodule, then  the module name followed by a do block.

This should be enough for us to define and use some example functions in IEx. 

We'll cover modules in greater detail in the Module section.

Function Example

Our example function is going to check if a provided term is a palindrome, and simply return true or false.  Our function will have an arity of 1 and can be described as: palindrome?/1

  • Arity describes the number of arguments a function accepts.
  • Functions are defined used the def keyword and the do / end combo.

Let's define the module Helper with the function palindrome?/1:

(Paste into IEx)

defmodule Helper do
def palindrome?(term) do
String.reverse(term) == term
end
end

palindrome?/1 accepts a term argument, reverses the term and returns the value of checked equality between the original term and the reversed term.

In Elixir, functions implicitly return the last expression.  

Calling the Example

To call the function, we'll need to call the Module and Function, passing in the argument.

Let's test a few strings:

iex> Helper.palindrome?("dog")
false

iex> Helper.palindrome?("abba")
true

In-Line Definition

When an implementation is very small you can define a function inline: (Paste into IEx)

defmodule Helper do
def palindrome?(term), do: String.reverse(term) == term
end

With inline definition, the end keyword is not required.

Function Guards

Sticking with our example, in the event that something other than a string is passed as the term argument, let's see what happens:

iex> Helper.palindrome?(1001)
** (FunctionClauseError) no function clause matching in String.Unicode.next_grapheme_size/1

Despite the value of term technically being a palindrome, an error is raised.

Let's add a guard to our function to ensure the value is a string: (Paste into IEx)

defmodule Helper do
def palindrome?(term) when is_binary(term) do
String.reverse(term) == term
end
end

Now if we try to pass a non-binary value as an argument:

iex> Helper.palindrome?(1001)
** (FunctionClauseError) no function clause matching in Helper.palindrome?/1

This gives a better error, no function clause is defined to handle integers.

Custom Guards

Elixir provides a construct to define custom guards but this section is focused on the basics of guards.  We'll cover custom guards in more detail in a later section.

Quick Example:

defmodule Integer.Guards do
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end

The custom guard can be used with: is_even(value)

https://hexdocs.pm/elixir/Kernel.html#defguard/1

Multi-Clause Functions

When you provide multiple clauses for a function with the same arity, Elixir will pattern match from the top down until it finds a matching definition.

We can extend our palindrome?/1 function to support integer values by adding another function clause with the same arity, but instead we'll use the is_integer guard.

(Paste into IEx)

defmodule Helper do
def palindrome?(term) when is_binary(term) do
String.reverse(term) == term
end

def palindrome?(term) when is_integer(term) do
term = Integer.to_string(term)
String.reverse(term) == term
end
end

Now if we pass the integer value:

iex> Helper.palindrome?(1001)
true

iex> Helper.palindrome?(1002)
false

Our Helper module now support strings and integers.

We've only used two guards in this example but there are many more available in Elixir's Kernel module:

https://hexdocs.pm/elixir/1.10.0/Kernel.html#guards

Because there is no "return" keyword to return early in Elixir functions, using multi-clause functions provides and excellent way to explicitly define how our program will handle different data types and situations and control the flow of execution.

Pattern Matching Arguments

Apart from using Guards, Elixir provides another feature that enables fine-grained function definition: Pattern Matching on function arguments.

Say we have a user map which includes the first name and last name:

iex> user_params = %{user: %{first_name: "tom", last_name: "jones"}}

And needed a function to combine the first and last name.

We could simply accept the user_params nested map and do the work, but what if the first and last name are missing, or the map's root key is not user?

Instead of wiring this logic and data validation into a single function, we can take advantage of Pattern Matching within the function definition: full_name/1:

defmodule UserHelper do
def full_name(%{user: user}) do
user.first_name <> " " <> user.last_name
end
end

Then to call it:

iex> UserHelper.full_name(user_params)
"tom jones"

Anything other than a map with the root, atom-based key: user will not match on this function.

We can also Pattern Match further into the nested map by targeting the first and last names:

defmodule UserHelper do
def full_name(%{user: %{first_name: first_name, last_name: last_name}}) do
first_name <> " " <> last_name
end
end

Then to call:

UserHelper.full_name(user_params)

This function works exactly the same way, but requires that user_params matches the pattern more precisely.

This pattern combined with guard clauses provides immense flexibility and granularity when defining a programs business logic.  It also promotes writing clearer functions which contain fewer lines in a more declarative style.

NOTE: You can also bind the full user_params argument to a variable in addition to pattern matching:

 def full_name(%{user: user} = user_params) do

Private Functions

In some cases you may need to restrict a function from being called outside of a module which is when you could define a private function.

Private functions are defined with the defp keyword.

Let's extend the previous example by capitalizing the first and last names before returning them:

defmodule UserHelper do
def full_name(%{user: user}) do
format_full_name(user)
end

defp format_full_name(%{first_name: first_name, last_name: last_name}) do
String.capitalize(first_name) <> " " <> String.capitalize(last_name)
end
end

full_name/1 pattern matches on the nested user map, then passes the flat map to format_full_name/1 which then pattern matches on the first/last names.

The names are capitalized separately and the concatenated full name is returned. 

This is a contrived example and there are likely better ways to accomplish this feat.  But this demonstrates how you might implement a private function.

Default Arguments

Elixir also supports default arguments which are assigned using: \\ 

Eg.

defmodule Hello do
def greeting(name \\ "world") do
IO.puts "Hello, " <> name <> "!"
end
end

iex> Hello.greeting()
Hello, world!

Wrapping Up

As first-class citizens, Functions are the heart and soul of Elixir which is why this section is dedicated to the many great features of defining and executing functions.

Here's what we've discovered:

  • How to write anonymous functions, including the capture-operator.
  • How to Implement guard clauses, and multi-clause functions.
  • How to use pattern matching on function arguments to define precise functions with a clear purpose, while controlling the flow of execution.
  • How to assign default values to function arguments.

I hope you found some value in this section and as we progress through the nuts and bolts of Elixir, you can start to see the flexibility and power of functional programming in Elixir.

Elixir Series

Connect

I hope you're finding this series helpful.  If you find any issues or have any feedback feel free to hit me up on twitter: @tmartin8080 || @phxroad  or subscribe to the mailing list to receive occasional updates.

Author image

About Troy Martin

Ruby, Elixir and Javascript Software Developer.