View Source Functions
Since Elixir is a functional programming language, functions are first-class citizens and the foundation for everything you build. You already saw how to define a function inside a module, but let's elaborate on this a bit.
You can define multiple function "clauses" that match against certain values. Function clauses are evaluated top-to-bottom, so the function clause at the top is evaluated first. If it doesn't match, the second function clause is evaluated, and so on. If one function matches, all functions that come thereafter are ignored.
defmodule RunElixir.Checker do
# This clause will be evaluated first.
# It matches against integer values from 18 to 150.
# The >= and <= operators are called 'Guards'.
def adult?(age) when is_integer(age) and age >= 18 and age <= 150 do
true
end
# This clause will match integer values from 0 to 17.
# If 'age' was '18', the function clause above would have matched
# and this function clause would never get evaluated.
def adult?(age) when is_integer(age) and age >= 0 do
false
end
# This function will match all values that didn't match the function clauses above.
# This could be integer values outside the range from 0 to 150 or other data types.
#
# This is also how you write a one-line function without the do ... end notation.
def adult?(age), do: raise "Invalid age: #{age}"
end
Default values
You can define default values using the \\
symbol, like this:
defmodule RunElixir.Profile do
@legal_age 18
def adult?(age, legal_age \\ @legal_age) do
age >= legal_age
end
end
Now, you can either use the default value or overwrite it like this:
RunElixir.Profile.adult?(18) # => true
RunElixir.Profile.adult?(18, 21) # => false
If you want to define a default value for multiple function clauses, you need to create a function head
, like this:
defmodule RunElixir.ProfileChecker do
@legal_age 18
# This is the function head which defines the default value for all clauses. It has no body.
def adult?(age, legal_age \\ @legal_age)
def adult?(age, legal_age) when age >= legal_age do
true
end
def adult?(age, legal_age) when age >= 0 and age < legal_age do
false
end
def adult?(age, _legal_age), do: raise("Invalid age: #{age}")
end
# Again, you can either use the default value or overwrite it when you call the function:
RunElixir.ProfileChecker.adult?(18) # => true
RunElixir.ProfileChecker.adult?(18, 21) # => false
Function Arity
Functions have an arity
, which describes how many arguments they expect. For example, our adult?
function has an arity of 2
because it expects two input arguments, so you would identify the function as adult?/2
.
However, one of the arguments has a default value, so we actually define two functions, one with an arity of 1
(plus the default value but it doesn't count) and one with an arity of 2
if we overwrite the default value.
If you list all functions of the Profile
module, you'd see that it has two adult?
functions:
RunElixir.Profile.__info__(:functions)
[adult?: 1, adult?: 2]
Anonymous Functions
You can define an anonymous function with fn arguments -> body end
.
add = fn a, b -> a + b end
add.(1, 2) # => 3
# A function without arguments
ran = fn -> Enum.random(1..100) end
ran.() # => 20
# You can also use anonymous functions as 'Closures' because they can 'close'
# around variables defined in the same scope and use them later.
value = 20
lazy_evaluate = fn div -> value * 10.0e10 / div end
lazy_evaluate.(2048) # => 976562500.0
# Even if you change the local variable after you define the anonymous function,
# the function will keep the old value.
value = 30
lazy_evaluate.(2048) # => 976562500.0
# A shorthand notation for anonymous functions:
# &1 is the first argument, &2 the second, and so on.
fun = &(&1 + &2)
fun.(4, 5) # => 9
You can assign an anonymous function to a variable and pass it around as an argument too. This comes in handy if you work with callbacks. In this example, we fetch a dad joke from an API using the HTTP library Req and execute the callback function with the response.
# First, we install the Req library.
Mix.install([{:req, "~> 0.5.0"}])
defmodule RunElixir.Jokes do
def get_dad_joke(callback_fn) do
# This will execute an anonymous function in an async process and
# execute the callback function with the result.
# We will discuss async functions later on.
spawn(fn ->
joke = Req.get!("https://icanhazdadjoke.com", headers: [accept: "text/plain"])
callback_fn.(joke)
end)
end
end
Now, let's see how you can pass an anonymous function as an argument.
callback_fn = fn
# You can define multiple function clauses also in anonymous functions:
%Req.Response{status: 200, body: joke} -> IO.inspect("Here's a dad joke for you: #{joke}")
%Req.Response{status: status, body: message} -> IO.inspect("Oh no! An error occurred #{status} - #{message}")
end
RunElixir.Jokes.get_dad_joke(callback_fn)
"Here's a dad joke for you: Why does Han Solo like gum? It's chewy!"
Return values
There is no explicit return
statement in Elixir and you cannot return early from a function. A function always returns the last statement in its body.
fun = fn age ->
if age >= 18, do: :adult
:minor
end
fun.(18) # => :minor
fun.(17) # => :minor
1: :minor
2: :minor
The default return value for a function is nil
, so if you don't specify a return value explicitly, Elixir will return nil
for you. This might surprise you in some cases:
empty_fun = fn -> end
fun = fn age ->
# The 'if ... end' statement will return 'nil' if it doesn't evaluate to 'true'
# and you don't provide an 'else' clause.
if age >= 18 do
:adult
end
end
empty_fun.() # => nil
fun.(18) # => :adult
fun.(17) # => nil
The only exception to this is if a function raises an exception. In that case, the rest of the function is not executed:
raises = fn ->
raise "Boom"
:return_something
end
raises.() |> IO.inspect()
** (RuntimeError) Boom