Struktury sterujące

To tłumaczenie jest aktualne.

W tej lekcji przyjrzymy się strukturom sterującym dostępnym w Elixirze.

Spis treści

if i unless

Zapewne spotkałeś się już z if/2 w innych językach, a jeżeli znasz język Ruby, to również unless/2 zapewne nie będzie Ci obce. W Elixirze instrukcje te działają w podobny sposób, ale nie są elementami języka, a makrami. Ich implementacje możesz znaleźć w dokumentacji modułu jądra.

Przypomnijmy, że w Elixirze jedynymi wartościami traktowanymi jako fałsznil i wartość logiczna false.

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

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

Użycie unless/2 jest podobne do if/2, tylko że warunek działa na odwrót:

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

case

Jeżeli chcemy sprawdzić wiele różnych wzorców, to możemy użyć case/2:

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

Zmienna _ jest istotnym elementem wyrażenia case/2. Bez niej, jeżeli nie będzie istnieć dopasowanie, program zwróci błąd:

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

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

Konstrukcję _ możesz rozumieć jako else – dopasowuje bowiem „wszystko inne”.

Jako że case/2 wykorzystuje dopasowanie wzorców, wszystkie zasady tam obowiązujące są zachowane. Jeżeli chcesz dopasować istniejącą zmienną, musisz użyć operatora przypięcia ^:

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"

Kolejną właściwością case jest wsparcie dla wyrażeń strażników:

Ten przykład pochodzi z oficjalnego przewodnika po języku Elixir Getting Started.

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

Więcej szczegółów znajdziesz w dokumentacji w języku angielskim, w module Expressions allowed in guard clauses.

cond

Jeżeli chcemy sprawdzić warunki niebędące wartościami, możemy użyć cond/1; wyrażenie to odpowiada konstrukcjom else if czy elsif z innych języków:

Ten przykład pochodzi z oficjalnego przewodnika po języku Elixir 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"

Podobnie jak case, cond zwróci błąd, jeżeli żadne z wyrażeń nie będzie spełnione. By obsłużyć taką sytuację, możemy jako warunek podać true:

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

with

Konstrukcji with/1 możemy użyć zamiast wielu zagnieżdżonych wyrażeń case/2 lub w sytuacjach, gdy nie mogą być one powiązane w jednoznaczny sposób. Wyrażenie with/1 składa się ze słowa kluczowego, generatora i wyrażenia.

Zajmiemy się jeszcze generatorami przy okazji omawiania list składanych, ale na chwilę obecną jedyne, co musimy wiedzieć, to że używają dopasowania wzorców, by połączyć elementy po prawej stronie <- z tymi po lewej.

Zacznijmy od prostego wyrażenia with/1:

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

W przypadku, kiedy żadne z wyrażeń nie zostanie dopasowane, zostanie zwrócona niepasująca wartość:

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

Teraz przyjrzyjmy się większemu przykładowi bez with/1, a następnie zrefaktoryzujmy go:

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

      error ->
        error
    end

  error ->
    error
end

Dzięki wprowadzeniu with/1 nasz końcowy kod jest krótszy i łatwiejszy do zrozumienia:

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 od wersji 1.3 pozwala też na użycie else w wyrażeniach with/1:

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

Pozwala to na łatwiejszą obsługę błędów, która jest podobna do wyrażenia case. Przekazywana wartość to pierwsze niedopasowane wyrażenie.

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