Do you want to pick up from where you left of?
Take me there

Керуючі конструкції

В цьому уроці ми розглянемо доступні в мові Elixir керуючі конструкції.

Table of Contents

if та unless

Скоріш за все, ви вже зустрічали оператор if/2, а якщо програмували на Ruby, то зустрічали і unless/2. В Elixir вони функціонують так само, але визначені як макрос, а не конструкція мови. Код реалізації можливо побачити в модулі Kernel.

Варто зазначити, що в Elixir єдиним хибними значеннями є nil та false.

iex> if String.valid?("Hello") do
...>   "Valid string!"
...> else
...>   "Invalid string."
...> end
"Valid string!"

iex> if "a string value" do
...>   "Truthy"
...> end
"Truthy"

unless/2 схожий на if/2, але працює навпаки:

iex> unless is_integer("hello") do
...>   "Not an Int"
...> end
"Not an Int"

case

Якщо потрібно зіставити з декількома зразками, використовується оператор case/2:

iex> case {:ok, "Hello World"} do
...>   {:ok, result} -> result
...>   {:error} -> "Uh oh!"
...>   _ -> "Catch all"
...> end
"Hello World"

Змінна _ є важливою частиною конструкції case/2. Без неї, у випадку відсутності знайденого зіставлення, станеться помилка:

iex> case :even do
...>   :odd -> "Odd"
...> end
** (CaseClauseError) no case clause matching: :even

iex> case :even do
...>   :odd -> "Odd"
...>   _ -> "Not Odd"
...> end
"Not Odd"

Змінну _ можливо розглядати як else, котрий буде зіставлений з чим завгодно. Так, як case/2 базується на зіставлені зі зразком, то всі ті ж обмеження і особливості продовжують діяти. Якщо потрібно провести зіставлення зі значенням змінної замість її присвоєння, використовується вже знайомий оператор ^/1:

iex> pie = 3.14
3.14
iex> case "cherry pie" do
...>   ^pie -> "Not so tasty"
...>   pie -> "I bet #{pie} is tasty"
...> end
"I bet cherry pie is tasty"

Другою цікавою можливістю case/2 є підтримка обмежувальних виразів:

Цей приклад взятий з офіційної документації Getting Started.

iex> case {1, 2, 3} do
...>   {1, x, 3} when x > 0 ->
...>     "Will match"
...>   _ ->
...>     "Won't match"
...> end
"Will match"

Також рекомендуємо почитати офіційну документацію про вирази, доступні в обмежувальних виразах.

cond

Коли потрібно перевіряти умови, а не значення, можна скористатися cond/1. Це схоже на else if чи elsif в інших мовах:

Цей приклад взятий з офіційної документації Getting Started.

iex> cond do
...>   2 + 2 == 5 ->
...>     "This will not be true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   1 + 1 == 2 ->
...>     "But this will"
...> end
"But this will"

Так само, як і case/2, cond/1 викличе помилку, якщо не пройде жоден з виразів. Для вирішення цієї проблеми можна визначити умову в true:

iex> cond do
...>   7 + 1 == 0 -> "Incorrect"
...>   true -> "Catch all"
...> end
"Catch all"

with

Спеціальна форма with/1 може знадобитися в ситуаціях, коли важко використовувати оператор потоку, або коли потрібен вкладений виклик case/2. with/1 складається з ключових слів, генераторів та виразу в кінці.

Ми ще поговоримо про генератори в уроці про спискові включення, але зараз нам достатньо знати лише те, що вони використовують зіставлення зі зразком для порівняння правої частини <- з лівою.

Почнемо з простого прикладу з with/1:

iex> user = %{first: "Sean", last: "Callan"}
%{first: "Sean", last: "Callan"}
iex> with {:ok, first} <- Map.fetch(user, :first),
...>      {:ok, last} <- Map.fetch(user, :last),
...>      do: last <> ", " <> first
"Callan, Sean"

У випадку, якщо для виразу не знайдеться збіг, повернеться значення, яке не збіглося:

iex> user = %{first: "doomspork"}
%{first: "doomspork"}
iex> with {:ok, first} <- Map.fetch(user, :first),
...>      {:ok, last} <- Map.fetch(user, :last),
...>      do: last <> ", " <> first
:error

Давайте глянемо на приклад побільше без використання with/1, а потім подивимось, як його можна покращити:

case Repo.insert(changeset) do
  {:ok, user} ->
    case Guardian.encode_and_sign(user, :token, claims) do
      {:ok, token, full_claims} ->
        important_stuff(token, full_claims)

      error ->
        error
    end

  error ->
    error
end

А тепер, завдяки with/1, ми отримаємо короткий та простий для розуміння код:

with {:ok, user} <- Repo.insert(changeset),
     {:ok, token, full_claims} <- Guardian.encode_and_sign(user, :token, claims) do
  important_stuff(token, full_claims)
end

Починаючи з версії Elixir 1.3, конструкція with/1 також почала підтримувати else:

import Integer

m = %{a: 1, c: 3}

a =
  with {:ok, number} <- Map.fetch(m, :a),
       true <- is_even(number) do
    IO.puts("#{number} divided by 2 is #{div(number, 2)}")
    :even
  else
    :error ->
      IO.puts("We don't have this item in map")
      :error

    _ ->
      IO.puts("It is odd")
      :odd
  end

Це допомагає структурувати код обробки помилок за допомогою зіставлення зі зразком в загальному блоці-обробнику. Значення, котре туди передаєтеся — перший же вираз, який не зіставився в основному тілі with.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!