ðĶFunctions
Functions are the core building blocks of Elixir. If you had tried the example above about modules, you would have tried creating your very first function. Unlike functions in Python and Javascript, functions cannot be declared at the top-level, outside of a module. If a function does not have a module, then it will be impossible to reference.
Functions are created using the def
macro, where the format is as follows: def <function_name>(<function_parameters>) do ... end
.
Return values
The return value of a function is automatically set as the final statement in the function body (i.e. no explicit return
is necessary):
Impure functions
Unlike stricter functional programming languages, Elixir does allow side effects (like printing or database editing) in a function:
However, it is recommended to keep side effects to a minimum.
Quality of life
Elixir also provides minor quality of life features when writing functions. Some of which include:
Omitting the parentheses when there are no parameters
Removing the use of
end
when there is only the return statement
Method overloading
Similar to other languages, Elixir also supports method overloading (i.e. declaring the same method name with differing parameters). For instance:
Default arguments
Elixir functions also supports default arguments:
The function above receives an optional argument b
that defaults to 0
if left unspecified. If b
is omitted, minus
just returns the original argument. Otherwise, it performs the subtraction operation.
Pattern matching
Function parameters can also be pattern matched, allowing you to create really concise function definitions without the need for any other constructs. For instance, look at the implementation of the Fibonacci sequence using pattern matching:
Pattern matching in functions makes it so we can easily express such recurrence relations with minimal effort and in a way that makes sense semantically.
You can combine the pattern matching shown in previous examples as well! For instance, you can pattern match the parts of a map and use them within the function without any further access methods:
If there are multiple function declarations with different pattern matching parameters, Elixir will try each of them until it finds a match. If no match is found, then an exception is raised. You can create a "base function" to handle such cases. These functions tend to be the last in the function list:
Try arranging your pattern matching functions in decreasing order of strictness (i.e. the most specific cases should be declared first)
Guard clauses
Another way to validate the arguments of a function before the function body is to use guard clauses which are added after the parameter list, following the when
keyword. This is useful if you wish to combine various pattern matching clauses, you have to check the types of the arguments, or when you need to validate the arguments against one another:
There are certain limitations to using guard clauses, one of which includes not being able to use custom functions in the guard clause (this is due to the nature of how guard clauses and functions are compiled). You can refer to this website for more information about guard clauses.
Anonymous functions
Anonymous functions are functions declared without using the def
macro. They allow you to pass functions around as parameters or return values (you can do that even with regular functions too!).
You can declare anonymous functions with fn <parameter list> -> <function body> end
:
You can also make the anonymous function multi-line by adding a newline after ->
(unlike Python's lambdas).
Notice that you call the anonymous function using .()
rather than just ()
, this helps make it clear that you are calling an anonymous function as you may have overriden an existing function.
Closures
Anonymous functions have access to the variables that are in scope when the function is defined. This is also known as a closure.
Currying
Closures are particularly useful when you are trying to curry functions. Currying is a functional programming technique that takes functions that accept multiple parameters and transforms them into functions that take only one parameter each.
Closures allow each of the nested functions to reference the variables from outside of its current scope (i.e. the innermost function can reference a
and b
):
Currying is useful for creating partial applications of functions. For instance, let's say you would like to apply the last operation (* c
) with different values but preserve the values of a = 1
and b = 2
from the initial application, you can do so by applying the curried function twice (not three times) and then saving the partially applied function as a variable:
So, rather than having to type foo(1, 2, 3)
and then foo(1, 2, 6)
and then foo(1, 2, 10)
, you only have to call partial.(x)
with the different variables.
Last updated