Условные переходы в функциональных языках отличаются от императивных, потому что основаны на сопоставлении с образцом. Основная идея в том, что некое значение по очереди сравнивается с несколькими шаблонами, и в зависимости от того, с каким шаблоном оно совпадет, выполняется та или иная ветка кода.
Есть несколько вариантов условных переходов:
case
;cond
;rescue
, catch
;receive
.Все они, кроме cond, реализуют эту идею.
Для примера рассмотрим вычисление наибольшего общего делителя:
def gcd(a, b) do
case rem(a, b) do
0 -> b
c -> gcd(b, c)
end
end
Здесь вычисляется значение rem(a, b)
и сравнивается с двумя шаблонами. Первый шаблон -- литерал 0
. Если значение совпадает с ним, то выполняется код, завершающий рекурсию и возвращающий b
. Второй шаблон -- переменная c
. С этим шаблоном совпадут любые значения, и тогда выполняется вызов gcd(b, c)
.
Второй пример:
case Map.fetch(acc, word) do
{:ok, count} -> Map.put(acc, word, count + 1)
:error -> Map.put(acc, word, 1)
end
Здесь выполняется вызов функции Map.fetch(acc, word)
. Получившееся значение сравнивается с двумя шаблонами и выполняется соответствующий код.
Шаблонов может быть несколько. И важен их порядок, потому что первый совпавший шаблон останавливает перебор оставшихся шаблонов. Если не совпал ни один из шаблонов, то генерируется исключение.
В общем виде конструкция case выглядит так:
case Expr do
Pattern1 [when GuardSequence1] ->
Body1
...
PatternN [when GuardSequenceN] ->
BodyN
end
Что такое GuardSequence -- цепочка охранных выражений, мы рассмотрим позже.
case могут быть вложенными друг в друга:
def handle(animal, action) do
case animal do
{:dog, name} ->
case action do
:add -> IO.puts("add dog #{name}")
:remove -> IO.puts("remove dog #{name}")
end
{:cat, name} ->
case action do
:add -> IO.puts("add cat #{name}")
:remove -> IO.puts("remove cat #{name}")
end
end
end
Вложенный даже на два уровня код плохо читается. Обычно этого можно избежать. Данный пример можно реализовать без вложенного case таким образом:
def handle(animal, action) do
case {animal, action} do
{{:dog, name}, :add} -> IO.puts("add dog #{name}")
{{:dog, name}, :remove} -> IO.puts("remove dog #{name}")
{{:cat, name}, :add} -> IO.puts("add cat #{name}")
{{:cat, name}, :remove} -> IO.puts("remove cat #{name}")
end
end
Теперь вернемся к упомянутым выше охранным выражениям.
Не всегда достаточно шаблона, чтобы проверить все условия для ветвления в коде. Например, шаблоном нельзя проверить попадание числа в определенный диапазон.
def handle4(animal) do
case animal do
{:dog, name, age} when age > 10 -> IO.puts("#{name} is a dog older than 10")
{:dog, name, _} -> IO.puts("#{name} is a 10 years old or younger dog")
{:cat, name, age} when age > 10 -> IO.puts("#{name} is a cat older than 10")
{:cat, name, _} -> IO.puts("#{name} is a 10 years old or younger cat")
end
end
Охранное выражение представляет собой предикат или цепочку предикатов:
when predicat1 and predicat2 or ... predicatN ->
В предикатах можно использовать ограниченный набор функций, описанный в документации. Некоторые функциональные языки разрешают вызывать любые функции в охранных выражениях. Но Эликсир не относится к таким языкам.
Если при вычислении охранного выражения возникает исключение, то оно не приводит к остановке процесса, а приводит к тому, что все выражение вычисляется в false. Это позволяет писать выражения проще. Вместо:
when is_map(a) and map_size(a) > 10 ->
можно сразу писать:
when map_size(a) > 10 ->
Реализовать функцию join_game(user)
, которая принимает игрока в виде кортежа {:user, name, age, role}
и определяет, разрешено ли данному игроку подключиться к игре. Если игроку уже исполнилось 18 лет, то он может войти в игру. Если роль игрока :admin
или :moderator
, то он может войти в игру независимо от возраста. Функция должна вернуть :ok
или :error
.
Реализовать функцию move_allowed?(current_color, figure)
которая определяет, разрешено ли данной шахматной фигуре сделать ход. Параметр current_color
может быть либо :white
либо :black
, и он указывает, фигурам какого цвета разрешено сделать ход. Параметр figure
представлен кортежем {type, color}
, где type
может быть один из: :pawn
, :rock
, :bishop
, :knight
, :queen
, :king
, а color может быть :white
или :black
. Фигура может сделать ход если её тип :pawn
или :rock
и её цвет совпадает с current_color
. Функция должна вернуть true
или false
.
Solution.join_game({:user, "Bob", 17, :admin})
# => :ok
Solution.join_game({:user, "Bob", 17, :moderator})
# => :ok
Solution.join_game({:user, "Bob", 17, :member})
# => :error
Solution.move_allowed?(:white, {:pawn, :white})
# => true
Solution.move_allowed?(:black, {:pawn, :white})
# => false
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1defmodule Test do
2 use ExUnit.Case
3 import Solution
4
5 test "join_game test" do
6 assert :ok == join_game({:user, "Bob", 17, :admin})
7 assert :ok == join_game({:user, "Bob", 27, :admin})
8 assert :ok == join_game({:user, "Bob", 17, :moderator})
9 assert :ok == join_game({:user, "Bob", 27, :moderator})
10 assert :error == join_game({:user, "Bob", 17, :member})
11 assert :ok == join_game({:user, "Bob", 27, :member})
12 end
13
14 test "move_allowed? test" do
15 assert move_allowed?(:white, {:pawn, :white})
16 assert not move_allowed?(:black, {:pawn, :white})
17 assert move_allowed?(:white, {:rock, :white})
18 assert not move_allowed?(:black, {:rock, :white})
19 assert not move_allowed?(:white, {:queen, :white})
20 assert not move_allowed?(:black, {:queen, :white})
21 end
22end
23
Решение учителя откроется через: