Функції
В Elixir, як і в багатьох інших функціональних мовах програмування, функції є повноцінними об’єктами. В цьому уроці ми розглянемо типи функцій в Elixir, чим вони відрізняються і як ними користуватись.
Анонімні функції
Як випливає з назви, у анонімної функції нема імені.
В уроці Enum
було показано що вони часто використовуються у якості параметрів інших функцій.
Для визначення анонімної функції в Elixir використовуються ключові слова fn
та end
.
Між ними можна визначити будь-яку кількість параметрів та тіл функції (function body), розділених ->
.
Давайте розглянемо простий приклад:
iex> sum = fn (a, b) -> a + b end
iex> sum.(2, 3)
5
Короткий синтаксис
Анонімні функції використовуються в мові дуже часто. Тому для них було створено спеціальне скорочення:
iex> sum = &(&1 + &2)
iex> sum.(2, 3)
5
Як ви вже могли здогадатися, у скороченій версії параметри доступні як &1
, &2
, &3
і так далі.
Зіставлення зі зразком
Зіставлення зі зразком в Elixir застосовується не тільки для зіставлення змінних. Цей же інструмент використовується для оголошення функцій.
Elixir використовує зіставлення зі зразком для перевірки всіх наборів параметрів і вибору першого відповідного набору для виконання:
iex> handle_result = fn
...> {:ok, result} -> IO.puts "Handling result..."
...> {:ok, _} -> IO.puts "This would be never run as previous will be matched beforehand."
...> {:error} -> IO.puts "An error has occurred!"
...> end
iex> some_result = 1
1
iex> handle_result.({:ok, some_result})
Handling result...
:ok
iex> handle_result.({:error})
An error has occurred!
Іменовані функції
Також в Elixir є можливість визначати іменовані функції для подальшого їх виклику за цими іменами.
Ці функції оголошуються за допомогою ключового слова def
в контексті модуля.
Більш детально ми розглянемо модулі в наступних уроках, а в цьому ми зосередимося тільки на іменованих функціях.
Функції, визначені в модулі, доступні з інших модулів:
defmodule Greeter do
def hello(name) do
"Hello, " <> name
end
end
iex> Greeter.hello("Sean")
"Hello, Sean"
Якщо функція поміщається в один рядок (однорядкова), то її опис можна скоротити, використовуючи do:
:
defmodule Greeter do
def hello(name), do: "Hello, " <> name
end
Вже розібравшись у зіставленні зі зразком, давайте розглянемо приклад рекурсії з використанням іменованих функцій:
defmodule Length do
def of([]), do: 0
def of([_ | tail]), do: 1 + of(tail)
end
iex> Length.of []
0
iex> Length.of [1, 2, 3]
3
Найменування та арність функцій
Раніше ми зазначали, що функції іменуються шляхом поєднання імені та арності (кількості аргументів). Це дозволяє робити такі речі:
defmodule Greeter2 do
def hello(), do: "Hello, anonymous person!" # hello/0
def hello(name), do: "Hello, " <> name # hello/1
def hello(name1, name2), do: "Hello, #{name1} and #{name2}"
# hello/2
end
iex> Greeter2.hello()
"Hello, anonymous person!"
iex> Greeter2.hello("Fred")
"Hello, Fred"
iex> Greeter2.hello("Fred", "Jane")
"Hello, Fred and Jane"
В коментарях до функцій ми привели їх найменування.
Перша функція не приймає жодних аргументів, тому описується як hello/0
; друга приймає один параметр, тому описується як hello/1
і т.д.
На відміну від перевантаження функцій в деяких інших мовах програмування, в нашому випадку функції варто вважати різними .
(Зіставлення зі зразком, яке ми описували раніше, застосовується тільки у випадку, коли для функцій з однаковою кількістю аргументів надається декілька різних реалізацій.)
Функції і зіставлення зі зразком
За лаштунками функції зіставляють зі зразком аргументи, з якими вони були викликані.
До прикладу, нам потрібно написати функцію, яка приймає асоціативний масив, і ми зацікавлені у використанні лише одного конкретного ключа. В такому разі ми можемо зіставити аргумент зі зразком таким чином, щоб перевірити наявність цього ключа:
defmodule Greeter1 do
def hello(%{name: person_name}) do
IO.puts "Hello, " <> person_name
end
end
Припустімо, що ми маємо асоціативний масив, який описує людину на ім’я Fred:
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }
Ось такі результати ми отримаємо, коли викличемо Greeter1.hello/1
з асоціативним масивом fred
:
# виклик з повним масивом
...> Greeter1.hello(fred)
"Hello, Fred"
Що трапиться, коли ми викличемо функцію з асоціативним масивом, що не містить ключ :name
?
# виклик без потрібного ключа повертає помилку
...> Greeter1.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter1.hello/1
The following arguments were given to Greeter1.hello/1:
# 1
%{age: "95", favorite_color: "Taupe"}
iex:12: Greeter1.hello/1
Причиною такої поведінки є те, що в Elixir зіставлення аргументів функції зі зразком викликається на арність, з якою ця функція визначена.
Давайте подивимося на те, як виглядають дані, коли вони надходять у Greeter1.hello/1
:
# вхідний асоціативний масив
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }
Greeter1.hello/1
очікує такого аргументу:
%{name: person_name}
В Greeter1.hello/1
переданий асоціативний масив (fred
) зіставляється з нашим аргументом (%{name: person_name}
):
%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}
Функція виявляє, що у вхідному асоціативному масиві є ключ, який відповідає на name
.
Зіставлення успішне! І як результат успішного зіставлення, значення ключа :name
з асоціативного масиву справа (наприклад асоціативний масив fred
) пов’язується зі змінною зліва (person_name
).
А тепер, що якби ми все ще хотіли присвоїти ім’я Fred змінній person_name
, але ми ТАКОЖ хотіли би зберегти знання про всю асоціативну мапу людини?
Скажімо, ми хочемо виконати IO.inspect(fred)
після привітання.
Наразі через те, що ми зіставили зі зразком лише ключ :name
із нашого асоціативного масиву, ми пов’язуємо лише значення цього ключа зі змінною - функція не має решти знань про Фреда.
Для того, щоб цю інформацію зберегти і могти використовувати, ми повинні присвоїти весь асоціативний масив окремій змінній.
Давайте напишемо нову функцію:
defmodule Greeter2 do
def hello(%{name: person_name} = person) do
IO.puts "Hello, " <> person_name
IO.inspect person
end
end
Пам’ятаймо, що Elixir буде на вході зіставляти аргумент зі зразком. Тому в цьому випадку кожна сторона буде зіставляти зі зразком вхідний аргумент і прив’язувати його до того, що зі зразком співпало. Для початку глянемо на праву сторону:
person = %{name: "Fred", age: "95", favorite_color: "Taupe"}
Зараз до змінної person
було прив’язано весь асоціативний масив fred
.
Далі подивимося на наступне зіставлення зі зразком:
%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}
Далі все таке ж, як в оригінальній функції Greeter1
, де ми зіставляли зі зразком асоціативний масив і залишали лише ім’я Фреда.
Нам вдалося отримати дві змінні, які ми можемо використовувати (на противагу одній в оригінальній функції):
-
person
зберігає дані%{name: "Fred", age: "95", favorite_color: "Taupe"}
-
person_name
зберігає стрічку"Fred"
Тому зараз, коли ми викликаємо Greeter2.hello/1
, ми можемо використовувати всю інформацію про Фреда:
# виклик з всією інформацією про людину
...> Greeter2.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}
# виклик лише з ключем імені
...> Greeter2.hello(%{name: "Fred"})
"Hello, Fred"
%{name: "Fred"}
# виклик без ключа імені
...> Greeter2.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter2.hello/1
The following arguments were given to Greeter2.hello/1:
# 1
%{age: "95", favorite_color: "Taupe"}
iex:15: Greeter2.hello/1
З цього ми бачимо, що в Elixir зіставлення зі зразком має неабияку глибину, оскільки кожен аргумент незалежно зіставляється з вхідними даними, залишаючи нам змінні, за якими ці дані можна викликати в нашій функції.
Якщо ми змінимо порядок %{name: person_name}
і person
в списку, то ми отримаємо ідентичний результат, оскільки вони співставляють fred
незалежно.
Ми обмінюємо змінну і асоціативний масив:
defmodule Greeter3 do
def hello(person = %{name: person_name}) do
IO.puts "Hello, " <> person_name
IO.inspect person
end
end
І викликаємо з тими ж даними, які використовували в Greeter2.hello/1
:
# викликаємо з тим же Фредом
...> Greeter3.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}
Пам’ятаймо, що хоча виглядає це так, ніби %{name: person_name} = person
зіставляє %{name: person_name}
зі змінною person
, насправді тут обидві сторони зіставляються з вхідним аргументом.
Підсумок: Функції незалежно зіставляють вхідні дані з кожним своїм аргументом. Ми можемо це використовувати для того, щоб прив’язувати значення до окремих змінних всередині функції.
Закриті функції
Якщо ми не хочемо давати доступ до функції з других модулів, ми визначаємо закриті (private) функції.
Вони можуть бути викликані тільки з того ж модуля. Такі функції визначаються за допомогою defp
:
defmodule Greeter do
def hello(name), do: phrase() <> name
defp phrase, do: "Hello, "
end
iex> Greeter.hello("Sean")
"Hello, Sean"
iex> Greeter.phrase
** (UndefinedFunctionError) function Greeter.phrase/0 is undefined or private
Greeter.phrase()
Обмежувачі
Ми вже стикалися з обмежувачами у розділі Керуючі конструкції, тепер давайте розглянемо їх застосування в іменованих функціях. Обмежувачі перевіряються тільки після того, як Elixir зіставив функцію.
В наступному прикладі у нас є дві функції з однаковими сигнатурами. Ми використовуємо обмежувачі для визначення, яку саме з них використовувати на основі типу аргументу:
defmodule Greeter 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
iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"
Аргументи за замовчуванням
Коли ми хочемо, щоб аргумент мав деяке значення за замовчуванням - використовується синтаксис argument \\ value
:
defmodule Greeter do
def hello(name, language_code \\ "en") do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("es"), do: "Hola, "
end
iex> Greeter.hello("Sean", "en")
"Hello, Sean"
iex> Greeter.hello("Sean")
"Hello, Sean"
iex> Greeter.hello("Sean", "es")
"Hola, Sean"
Коли ми застосовуємо одночасно обмежувачі та аргументи за замовчуванням, то все перестає працювати. Давайте подивимось як це виглядає:
defmodule Greeter 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
** (CompileError) iex:31: definitions with multiple clauses and default values require a header. Instead of:
def foo(:first_clause, b \\ :default) do ... end
def foo(:second_clause, b) do ... end
one should write:
def foo(a, b \\ :default)
def foo(:first_clause, b) do ... end
def foo(:second_clause, b) do ... end
def hello/2 has multiple clauses and defines defaults in one or more clauses
iex:31: (module)
Elixir не підтримує аргументи за замовчуванням при наявності декількох підходящих функцій. Для вирішення цієї проблеми ми додаємо визначення функції з аргументами за замовчуванням:
defmodule Greeter 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
iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"
iex> Greeter.hello ["Sean", "Steve"], "es"
"Hola, Sean, Steve"
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!