Functions in Elixir accept parameters and return values like you would expect, but since Elixir is a functional language, the language is oriented around functions.
The typical function in Elixir is short and simple, and easily tested. Those simple functions are then composed, which is to say they are combined with each other, to make other functions. By keeping functions small and simple, and by calling other small and simple functions, Elixir programs tend to be easy to understand (and just as importantly) easy to write tests for.
Functions are first-class citizens, which means they can be passed around and returned from other functions just like any data variable. Javascript has had this concept of first-class functions from the beginning, but the ability to assign functions to variable and pass them and return them from functions is a more recent addition to C#.
In C#, function parameters are always statically-typed just like everything else, and in Javascript no types are associated with parameters. Elixir function parameters are dynamically-typed like Javascript, and the type of each parameter is not declared in the code.
Functions typically live in modules, which is how functions are grouped in Elixir.
Elixir Source Files
This is where we start making use of Elixir source code files, which end in .exs and .ex. Since I currently have no idea of how to build .ex Elixir source code files, I'm going to be using .exs files, which can be loaded into IEx and interpreted.
So some of the examples from here on out will be from Elixir source code files, while others are coming from what I've typed into IEx. You'll be able to tell by the presence or absence of the "IEx>" prompt.
Modules
Modules encapsulate and organize functions in Elixir. Whereas object-oriented languages typically contain data and the functions that operate on that data, in Elixir, functions and data are kept separate.
Related functions are grouped together in a module. Functions that operate on the same data structure are typically kept together in their own module. For example, all the string manipulation functions in the Elixir standard library are grouped together in the String
module and all functions relating to lists are in the List
module.
Let's take a look at the syntax needed to define a module. A module is defined using the defmodule
keyword with the contents of the module enclosed between do
and end
.
Modules can be defined inline in IEx, but it's really ugly and cumbersome to do so.
iex> defmodule ExampleModule do
...> end
{:module, ExampleModule,
<<70, 79, 82, 49, 0, 0, 3, 36, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 127,
0, 0, 0, 12, 20, 69, 108, 105, 120, 105, 114, 46, 69, 120, 97, 109, 112, 108,
101, 77, 111, 100, 117, 108, 101, 8, 95, ...>>, nil}
Instead, a module is usually defined in a source code file instead.
defmodule ExampleModule do
end
Then name of this module is "ExampleModule" and the module body starts with the do
keyword and finishes with the end
keyword.
Modules can also be namespaced as "[namespace2].[module]" or "[namespace1].[namespace2].[module]". This allows modules with the same name to coexist in different namespaces.
defmodule Cars.Driving do
end
or
defmodule Vehicles.Cars.Driving do
end
I was curious to see if modules can be nested. It turns out that they can be.
defmodule Cars.Driving do
end
is equivalent to
defmodule Cars do
defmodule Driving do
end
end
A module must be defined in a single file: it cannot be distributed among multiple source code files. However, you can define multiple modules in a single file.
Interesting fact: module names are converted into atoms internally (in the Erlang VM) and have "Elixir" added to the front. So the module Stuff
is actually represented as :"Elixir.Stuff"
internally. (Atoms need to have quotes around them when they contain a special character like a space or a dot)
From the few examples I've seen, Erlang module names are also atoms, and I suspect that Elixir does this because the Erlang VM needs to have an atom for modules. Anyone monitoring the modules in the Erlang VM can see which modules come from the Elixir environment because they have "Elixir" prepended to their names.
Named Functions
An Elixir function is typically named, and is called a "named function". Functions are defined with the def
keyword followed by a parameter list. A function also has a body containing the contents of the function. Like modules, the function body begins with the do
keyword and ends with the end
keyword.
defmodule ExampleModule do
def do_something(parameter1, parameter2) do
end
end
The example above defines a function called do_something
in a module called ExampleModule
. This function accepts two parameters, parameter
and parameter2
. It does not return anything because we haven't put anything in the function body.
The value of the last expression in a function is the value that is returned from a function. Let's look at an example function that adds two numbers together.
def sum(num1, num2) do
num1 + num2
end
Unlike C# or Javascript, there is no return
keyword necessary. The value of the last statement is automatically returned from the function.
We can define a function that returns the parameter that was passed to it.
def echo(parameter) do
parameter
end
Named functions must always be defined in the context of a module! A named function cannot exist outside of a module.
Single-line Functions
If your function is very simple and has only one line, there is a more concise syntax available. We'll write the subtract
function using the more concise syntax.
def subtract(num1, num2), do: num1 - num2
The comma that separates the function name and parameters from the body, and tells Elixir to expect a single-line function body.
Function Names
In Elixir, named functions are specified by their name and their arity. Arity is a term that is thrown around a lot in functional programming. Simply put, the arity of a function is the number of parameters accepts. So the sum
function I showed you above has an arity of 2, since it accepts 2 parmaeters.
The Elixir function naming format is [name]/[arity], so the sum
function above is known to Elixir as sum/2
.
The arity is an integral part of a function's identity in Elixir, so Elixir sees function do_something/1
as a completely separate function than do_something/2
.
It's quite common to have Elixir functions with the same name, but a different arity. The functions in the Elixir standard library documentation are listed with their arity because that is an essential part of the function's identity.
I was curious what would happen if I specified two functions with the same name and arity in a module, like so:
defmodule ExampleModule do
def do_something(parameter1) do
"Carrot"
end
def do_something(parameter1) do
parameter1
end
end
I was expecting an error to be thrown, but it turns out that Elixir will load a module with only a warning indicating that the second function will never be used. When do_something/1
is called, only the first instance of the function defined will be called and the second instance of the function will be ignored. Interesting.
Anonymous Functions
An anonymous function is one that is not associated with a name. Anonymous functions, which are called "lambdas" in many languages, are typically defined and then assigned to a variable or passed to another function.
In Javascript, we typically do something like the following example.
var add = function(x, y) {
return x + y;
};
array.map(function(number) {
return number * 2;
});
The anonymous function is created and assigned to a variable. It could also be passed to another function like the map function in an array.
Here's the same thing in ES6 lambda syntax.
const add = (x, y) => x + y;
array.map(number => number * 2);
C# also has lambdas, which are often used in modern C# code.
Func<int, int, int> add = (int x, int y) => x + y;
var transformedList = list.Select(number => number * 2).ToList();
Anonymous functions in Elixir start with fn
and end with end
. The function parameters and body are separated by a ->
.
iex> add = fn (x, y) -> x + y end
#Function<12.99386804/2 in :erl_eval.expr/5>
The above function will accept two parameters and return the sum of those two parameters.
The parentheses around the function parameters can also be omitted
iex> add = fn x, y -> x + y end
#Function<12.99386804/2 in :erl_eval.expr/5>
Anonymous functions are typically short-lived functions that apply in a certain situation and are not reused. If you're finding yourself specifying the same anonymous function in several different places, you may want to consider turning it into a named function.
An anonymous function without parameters can be defined by not putting anything between fn
and ->
.
iex> hello = fn -> "hello" end
#Function<20.99386804/0 in :erl_eval.expr/5>
The above function will simply return the value "Hello".
Although anonymous functions typically contain a single expression, it is possible to define one with multiple expressions. As with any other function in Elixir, the value of the last expression is the value returned from the function.
iex> double_and_add = fn x ->
...> d = x * 2
...> d + 1
...> end
#Function<6.99386804/1 in :erl_eval.expr/5>
Elixir knows that the function hasn't ended until it sees the end
keyword. You can also put both expressions on the same line by using a semicolon.
iex> double_and_add = fn x -> d = x * 2; d + 1 end
#Function<6.99386804/1 in :erl_eval.expr/5>
I would advise against putting two expressions one line like that. It's a lot harder to read. In fact, if your function is going to be more complex than a single expression, you may want to consider making it a named function. Simplicity and clarity are preferred.
Shortcuts
Anonymous functions have a shortcut syntax that makes anonymous functions even more concise.
The addition function we specified above, fn (x, y) -> x + y end
can be rewritten as &(&1 + &2)
. The whole thing must be enclosed by &()
and the expression contained within refers to parameters by number. So &1
corresponds to the first parameter, &2
corresponds to the second parameter, and so on. Elixir takes a look at the expression and realizes that this is function with two parameters.
iex> add = &(&1 + &2)
&:erlang.+/2
I found the result of this particular anonymous function to be particularly interesting and unexpected. Instead of returning the usual #Function<... in :erl_eval.expr/5>
that IEx typically displays for an anonymous function, IEx displayed &:erlang.+/2
. It looks to me like it saw that the function I defined is the exact same function as the +
operator with an arity of 2 in the underlying Erlang environment.
So instead of creating a duplicate copy of an existing function, Elixir just bound my add
variable to the Erlang +
operator instead, which probably is a function like any other function. Elixir is able to detect when two functions are the same and reuses the alread-existing function. That saves memory and makes my code more efficient. I'm impressed that Elixir was able to do this.
I was curious if I could try creating an add function that adds parameters 1 and 3, but that isn't possible. It turns out you can't skip using any parameter numbers. In order for this to work, a &2
need to be used somewhere in the expression.
iex> add = &(&1 + &3)
** (CompileError) iex:13: capture &3 cannot be defined without &2
(elixir) src/elixir_fn.erl:126: :elixir_fn.validate/4
(elixir) src/elixir_fn.erl:117: :elixir_fn.capture_expr/5
You can reuse a parameter multiple times in the same expression. Below is an example of a function that returns the square of a number.
square = &(&1 * &1)
#Function<6.99386804/1 in :erl_eval.expr/5>
The parentheses around the shortcut function can be replaced by other delimiters in certain circumstances.
For example:
create_tuple = &({&1, &2})
This is an anonymous function that constructs a tuple from the first two parameters. In this case the parentheses can be left off.
create_tuple = &{&1, &2}
Lets look at several possible variations.
Tuples:
iex> create_tuple = &({&1, &2})
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_tuple.(3, 4)
{3, 4}
iex> create_tuple = &{&1, &2}
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_tuple.(3, 4)
{3, 4}
Lists:
iex> create_list = &[&1, &2]
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_list.(3, 4)
[3, 4]
Maps:
iex> create_map = &%{first: &1, second: &2}
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_map.(3, 4)
%{first: 3, second: 4}
Strings:
iex> create_string = &"The first parameter is #{&1} and the second parameter is #{&2}"
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_string.(3, 4)
"The first parameter is 3 and the second parameter is 4"
This will probably also work for other literals like binary literals or character lists.
You should use shortcut syntax for short, simple functions. Otherwise, shortcut syntax can make it more difficult to read your code, since parameters are referred to by number and not by name.
Referring to a Named Function
Anonymous functions can be passed to other functions or bound to a variable just like they are. Named functions can't. They need to be referred to with the capture operator &
.
iex> func = &ExampleModule.add/2
&ExampleModule.add/2
I'm not sure why this is necessary, but I assume that it probably resolves some ambiguity that would otherwise occur.
Calling Functions
Defining functions without being able to call them is pretty useless, so next we're going to look at how to call them.
There are two ways to call a named function in Elixir. You can either specify the function name with parentheses surrounding the parameters or you can separate the parameters from the function name with a space.
ExampleModule.do_something(parameter1, parameter2)
ExampleModule.do_something parameter1, parameter2
Which style you choose to use is up to you. Different Elixir developers will use different styles. There are a few circumstances in Elixir where the spacing style is too ambiguous and parentheses are required, but spaces work most of the time. I'm personally a bit more comfortable with the parentheses call style, since that's what I'm used to, coming from using Javascript and C#.
If you just specify a named function without arguments, like ExampleModule.do_something
, Elixir assumes that this is a function call. Parentheses are optional, so this is equivalent to ExampleModule.do_something()
.
Anonymous functions need to be called with a dot and parentheses.
iex> do_something = fn (x) -> x + 1 end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex> do_something.(3)
4
The difference in call syntax between anonymous functions and named functions really annoyed me at first. Then I came to realize that this syntax didn't really apply to just anonymous functions, but also named functions that had been bound to a variable.
In this example, I created a named function named echo
with an arity of 1 that returns the same value that was passed into it. I can call the named function without the dot. If I use the capture operator to assign the function to a variable, I must to call it with dot notation.
iex> ExampleModule.echo("Bob")
"Bob"
iex> echoFunc = &ExampleModule.echo/1
&ExampleModule.echo/1
iex> echoFunc("Bob")
** (CompileError) iex:19: undefined function echoFunc/1
iex> echoFunc.("Bob")
"Bob"
I'm not sure exactly why function variables need to use dot notation to make a function call, but there's probably some practical reason, like making sure variables are not confused with named functions in the same namespace. In the example above, when I tried to call a function variable without dot notation, Elixir doesn't recognize it as a variable, but attempts to find a function with the same name as the variable. The dot notation must indicate to Elixir that this is a variable and not a named function.
So I'm not as annoyed as I was, but I do still wish the syntax was consistent. I suppose that this decision could have been made by the language designer for other reasons I haven't seen yet, so I'm not going to jump to conclusions at this early stage. There's still much to learn about Elixir.
Loading a Module into IEx
I haven't mentioned how to load a module into IEx, so I'll cover that before I bring this post to an end. The c
command loads and compiles a file containing a module.
So I loaded the ExampleModule
module by typing c "example_module.exs
or `c("example_module.exs"). IEx then loads and compiles the source file in memory, making available to use in IEx. I'll demonstrate using this in the next post, where I plan to put what we know into practice by writing a module and using it in IEx.
You can find in some of this code in the the "lwm 11 - Modules and Functions Part 1" folder in the code examples in the Learn With Me: Elixir repository on Github.
By the way, if you want to learn about IEx commands or Elixir keywords and modules, you can use the h
function. I typed in h c
to find out more about the c
command, which is actually an Elixir function available in IEx.
iex> h c
def c(files, path \\ :in_memory)
Compiles the given files.
It expects a list of files to compile and an optional path to write the
compiled code to. By default files are in-memory compiled. To write compiled
files to a current directory use empty string "" for the path. When compiling
one file, there is no need to wrap it in a list.
It returns the names of the compiled modules.
If you want to recompile an existing module, check r/1 instead.
## Examples
iex> c(["foo.ex", "bar.ex"], "ebin")
[Foo, Bar]
iex> c("baz.ex")
[Baz]
But Wait, There's More!
That's right, we're not quite done with the topic of functions and modules yet. There was so much to talk about, I had to divide this topic into multiple posts. In the next post, we'll learn some more and then I'll practice what I've learned so far by creating a simple code module and calling its functions from IEx.