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