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
- Elixir 1.10.0
Elixir supports anonymous functions, sometimes called lambdas.
They're created using the fn keyword:
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:
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.
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:
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).
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".
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.
This is usually called a Closure: the function captures the memory locations of all variables used within it.
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).
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:
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.
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)
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:
When an implementation is very small you can define a function inline: (Paste into IEx)
With inline definition, the end keyword is not required.
Sticking with our example, in the event that something other than a string is passed as the term argument, let's see what happens:
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)
Now if we try to pass a non-binary value as an argument:
This gives a better error, no function clause is defined to handle integers.
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.
The custom guard can be used with: is_even(value)
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)
Now if we pass the integer value:
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:
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:
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:
Then to call it:
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:
Then to call:
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:
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:
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.
Elixir also supports default arguments which are assigned using: \\
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.