Рассмотрим оператор with
, зачем нужен и как используется. Для начала, опишем пример:
defmodule Example do
require Integer
def inc_even(num) do
if Integer.is_even(num) do
num + 1
else
{:error, :not_even}
end
end
def stringify_odd(num) do
if Integer.is_odd(num) do
Integer.to_string(num)
else
{:error, :not_odd}
end
end
end
Проблема в том, что при попытке скомпоновать эти функции через оператор пайплайна |>
будут возникать ошибки, если аргумент окажется неподходящим:
2 |> Example.inc_even() |> Example.stringify_odd()
# => "3"
2 |> Example.stringify_odd() |> Example.inc_even()
# => {:error, :not_even}
Можно попытаться решить проблему через case
:
case Example.inc_even(3) do
{:error, reason} -> reason
result ->
case Example.stringify_odd(result) do
{:error, reason} -> reason
stringified -> stringified
end
end
# => :not_even
Однако, такой подход начинает хуже работать с увеличением вложенности операций. Для выпрямления таких вычислений используется конструкция with
, с помощью которой описывается удачный путь вычислений (happy path) и комбинировать вызовы функций с разным форматом данных. Перепишем на with
:
with incremented <- Example.inc_even(2),
stringified <- Example.stringify_odd(incremented),
do: stringified
# => "3"
# по сути получился аналог
# 2 |> Example.inc_even() |> Example.stringify_odd()
Оператор with
полезен тем, что можно обрабатывать неудачные вызовы функций в ветке else
:
with incremented <- Example.inc_even(3),
stringified <- Example.stringify_odd(incremented) do
stringified
else
{:error, reason} -> reason
end
# => :not_odd
Внутри with
можно использовать разные функции с разными возвращаемыми значениями, например:
user = %{name: "John"}
with updated_user <- Map.put(user, :age, 20),
true <- updated_user[:age] == 20,
%{hobby: hobby} <- Map.put(updated_user, :hobby, "diving"),
do: hobby
# => "diving"
Для функции предиката добавим еще одну ветку и поменяем значение по ключу age
:
user = %{name: "John"}
with updated_user <- Map.put(user, :age, 22),
true <- updated_user[:age] == 20,
%{hobby: hobby} <- Map.put(updated_user, :hobby, "diving") do
hobby
else
false -> "incorrect age"
end
# => "incorrect age"
С помощью with
проще структурировать ошибки, которые могут возникнуть в цепочке вычислений, однако важно не увлечься при описании такой цепочки, потому что с ростом операций становится тяжелее понять что происходит с данными. В таких случаях лучше сгруппировать часть операции в отдельные функции и в итоговом with
описать меньшее количество вызываемых функций. Хорошим примером является фреймворк Phoenix, в котором при генерации ресурса, например пользователя, создается такой код для контроллера:
def create(conn, params) do
with {:ok, user} <- Users.create_user(params) do
conn
|> put_status(:created)
|> render("show.json", user: user)
end
На верхнем уровне Phoenix перехватывает ошибки создания ресурса, так как оно типично: {:error, changeset}
. Поэтому нет необходимости описывать отдельно ветку else
.
Реализуйте функцию validate
, которая проверяет переданный аргумент на следующие условия:
Примеры работы функции:
Solution.validate("some")
# => {:ok, "some"}
Solution.validate("hello!!")
# => {:ok, "hello!!"}
Solution.validate(1)
# => {:error, :not_binary}
Solution.validate("a")
# => {:error, :too_short}
Solution.validate("hello, world!")
# => {:error, :too_long}
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1defmodule Test do
2 use ExUnit.Case
3
4 describe "validate work" do
5 test "with valid data" do
6 assert Solution.validate("some") == {:ok, "some"}
7 assert Solution.validate("hello!!") == {:ok, "hello!!"}
8 end
9
10 test "with invalid data" do
11 assert Solution.validate(1) == {:error, :not_binary}
12 assert Solution.validate("a") == {:error, :too_short}
13 assert Solution.validate("hello, world!") == {:error, :too_long}
14 end
15 end
16end
17
Решение учителя откроется через: