Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Functions

elixier_notebooks/Functions.livemd

Functions

Anonymous Functions

Just as the name implies, an anonymous function has no name. To define an anonymous function in Elixir we need the fn and end keywords. Within these we can define any number of parameters and function bodies separated by ->.

sum = fn a, b -> a + b end
sum.(20, 80)
100

The & Shorthand

Using anonymous functions is such a common practice in Elixir there is shorthand for doing so:

mul = &(&1 * &2)
mul.(2, 3)
6

As you probably already guessed, in the shorthand version our parameters are available to us as &1, &2, &3, and so on.

Pattern Matching

Pattern matching isn’t limited to just variables in Elixir, it can be applied to function signatures as we will see in this section.

Elixir uses pattern matching to check through all possible match options and select the first matching option to run:

handle_result = fn
  {:ok, result} -> IO.puts("Handling #{result}...")
  {:ok, _} -> IO.puts("This would never run as previous will be matched beforehand")
  {:error} -> IO.puts("An error has occurred!")
end
#Function<42.105768164/1 in :erl_eval.expr/6>
some_result = 1
handle_result.({:ok, some_result})
Handling 1...
:ok
handle_result.({:error})
An error has occurred!
:ok

Named Functions

We can define functions with names so we can easily refer to them later. Named functions are defined within a module using the def keyword . We’ll learn more about Modules in the next lessons, for now we’ll focus on the named functions alone.

Functions defined within a module are available to other modules for use. This is a particularly useful building block in Elixir:

defmodule GreeterA do
  def hello(name) do
    "Hello, " <> name
  end
end

GreeterA.hello("Sean")
"Hello, Sean"

If our function body only spans one line, we can shorten it further with do: :

defmodule GreeterB do
  def hello(name), do: "Hello, " <> name
end
{:module, GreeterB, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:hello, 1}}

Armed with our knowledge of pattern matching, let’s explore recursion using named functions:

defmodule Length do
  def of([]), do: 0
  def of([_ | tail]), do: 1 + of(tail)
end
{:module, Length, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:of, 1}}
Length.of([])
0
Length.of([1, 2, 3])
3

Function Naming and Arity

We mentioned earlier that functions are named by the combination of given name and arity (number of arguments). This means you can do things like this:

defmodule GreeterC do
  # hello/0
  def hello(), do: "Hello, anonymous person!"
  # hello/1
  def hello(name), do: "Hello, " <> name
  def hello(name1, name2), do: "Hello, #{name1} and #{name2}"
  # hello/2
end
{:module, GreeterC, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:hello, 2}}
GreeterC.hello()
"Hello, anonymous person!"
GreeterC.hello("Fred")
"Hello, Fred"
GreeterC.hello("Fred", "Jane")
"Hello, Fred and Jane"

We’ve listed the function names in comments above. The first implementation takes no arguments, so it is known as hello/0; the second takes one argument so it is known as hello/1, and so on. Unlike function overloads in some other languages, these are thought of as different functions from each other. (Pattern matching, described just a moment ago, applies only when multiple definitions are provided for function definitions with the same number of arguments.)

Functions and Pattern Matching

Behind the scenes, functions are pattern-matching the arguments that they’re called with.

Say we needed a function to accept a map but we’re only interested in using a particular key. We can pattern-match the argument on the presence of that key like this:

defmodule GreeterD do
  def hello(%{name: person_name}) do
    IO.puts("Hello, " <> person_name)
  end
end
{:module, GreeterD, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:hello, 1}}
fred = %{
  name: "Fred",
  age: "95",
  favorite_color: "Taupe"
}
%{name: "Fred", age: "95", favorite_color: "Taupe"}

These are the results we’ll get when we call Greeter1.hello/1 with the fred map:

GreeterD.hello(fred)
Hello, Fred
:ok

What happens when we call the function with a map that doesn’t contain the :name key?

Greeter1.hello(%{age: "95", favorite_color: "Taupe"})

The reason for this behavior is that Elixir pattern-matches the arguments that a function is called with against the arity the function is defined with.

Let’s think about how the data looks when it arrives to Greeter1.hello/1:

# incoming map
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }

Greeter1.hello/1 expects an argument like this:

%{name: person_name}

In Greeter1.hello/1, the map we pass (fred) is evaluated against our argument (%{name: person_name}):

%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}
%{name: "Fred", age: "95", favorite_color: "Taupe"}

It finds that there is a key that corresponds to name in the incoming map. We have a match! And as a result of this successful match, the value of the :name key in the map on the right (i.e. the fred map) is bound to the variable on the left (person_name).

Now, what if we still wanted to assign Fred’s name to person_name but we ALSO want to retain awareness of the entire person map? Let’s say we want to IO.inspect(fred) after we greet him. At this point, because we only pattern-matched the :name key of our map, thus only binding the value of that key to a variable, the function doesn’t have knowledge of the rest of Fred.

In order to retain it, we need to assign that entire map to its own variable for us to be able to use it.

Let’s start a new function:

defmodule GreeterE do
  def hello(%{name: person_name} = person) do
    IO.puts("Hello, " <> person_name)
    IO.inspect(person)
  end
end
{:module, GreeterE, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:hello, 1}}

Remember that Elixir will pattern match the argument as it comes in. Therefore in this case, each side will pattern match against the incoming argument and bind to whatever it matches with. Let’s take the right side first:

person = %{name: "Fred", age: "95", favorite_color: "Taupe"}
%{name: "Fred", age: "95", favorite_color: "Taupe"}

Now, person has been evaluated and bound to the entire fred-map. We move on to the next pattern-match:

%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}
%{name: "Fred", age: "95", favorite_color: "Taupe"}

Now this is the same as our original GreeterD function where we pattern matched the map and only retained Fred’s name. What we’ve achieved is two variables we can use instead of one

  1. person, referring to %{name: "Fred", age: "95", favorite_color: "Taupe"}
  2. person_name, referring to "Fred"

So now when we call GreeterE.hello/1, we can use all of Fred’s information:

GreeterE.hello(fred)
Hello, Fred
%{name: "Fred", age: "95", favorite_color: "Taupe"}
%{name: "Fred", age: "95", favorite_color: "Taupe"}
GreeterE.hello(%{name: "Fred"})
Hello, Fred
%{name: "Fred"}
%{name: "Fred"}
GreeterE.hello(%{age: "95", favorite_color: "Taupe"})

So we’ve seen that Elixir pattern-matches at multiple depths because each argument matches against the incoming data independently, leaving us with the variables to call them by inside our function.

If we switch the order of %{name: person_name} and person in the list, we will get the same result because each are matching to fred on their own.

We swap the variable and the map:

defmodule GreeterF do
  def hello(person = %{name: person_name}) do
    IO.puts("Hello, " <> person_name)
    IO.inspect(person)
  end
end
{:module, GreeterF, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:hello, 1}}

And call it with the same data we used in GreeterE.hello/1:

GreeterF.hello(fred)
Hello, Fred
%{name: "Fred", age: "95", favorite_color: "Taupe"}
%{name: "Fred", age: "95", favorite_color: "Taupe"}

Remember that even though it looks like %{name: person_name} = person is pattern-matching the %{name: person_name} against the person variable, they’re actually each pattern-matching to the passed-in argument.

Summary: Functions pattern-match the data passed in to each of its arguments independently. We can use this to bind values to separate variables within the function.

Private Functions

When we don’t want other modules accessing a specific function we can make the function private. Private functions can only be called from within their own Module. We define them in Elixir with defp:

defmodule GreeterG do
  def hello(name), do: phrase() <> name
  defp phrase, do: "Hello, "
end
{:module, GreeterG, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:phrase, 0}}
GreeterG.hello("Sean")
"Hello, Sean"
GreeterG.phrase()

Guards

Once Elixir has matched a function any existing guards will be tested.

In the following example we have two functions with the same signature, we rely on guards to determine which to use based on the argument’s type:

defmodule GreeterH do
  def hello(names) when is_list(names) do
    names = Enum.join(names, ", ")

    hello(names)
  end

  def hello(name) when is_binary(name) do
    phrase() <> name
  end

  defp phrase, do: "Hello, "
end
{:module, GreeterH, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:phrase, 0}}
GreeterH.hello(["Sean", "Steve"])
"Hello, Sean, Steve"

Default Arguments

If we want a default value for an argument we use the argument \\ value syntax:

defmodule GreeterI do
  def hello(name, language_code \\ "en") do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end
{:module, GreeterI, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:phrase, 1}}
GreeterI.hello("Sean", "en")
"Hello, Sean"
GreeterI.hello("Sean")
"Hello, Sean"
GreeterI.hello("Sean", "es")
"Hola, Sean"

When we combine our guard example with default arguments, we run into an issue. Let’s see what that might look like:

defmodule GreeterJ do
  def hello(names, language_code \\ "en") when is_list(names) do
    names = Enum.join(names, ", ")

    hello(names, language_code)
  end

  def hello(name, language_code \\ "en") when is_binary(name) do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end
error: def hello/2 defines defaults multiple times. Elixir allows defaults to be declared once per definition. Instead of:

    def foo(:first_clause, b \\ :default) do ... end
    def foo(:second_clause, b \\ :default) do ... end

one should write:

    def foo(a, b \\ :default)
    def foo(:first_clause, b) do ... end
    def foo(:second_clause, b) do ... end

  Ayanami-sTower/Elixier-Notebooks/Functions.livemd#cell:vcwzfolp5tgtlk4w:8

Elixir doesn’t like default arguments in multiple matching functions, it can be confusing. To handle this we add a function head with our default arguments:

defmodule GreeterK do
  def hello(names, language_code \\ "en")

  def hello(names, language_code) when is_list(names) do
    names = Enum.join(names, ", ")

    hello(names, language_code)
  end

  def hello(name, language_code) when is_binary(name) do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end
{:module, GreeterK, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:phrase, 1}}
GreeterK.hello(["Sean", "Steve"])
"Hello, Sean, Steve"
GreeterK.hello(["Sean", "Steve"], "es")
"Hola, Sean, Steve"