Elixir: Оператор with
Рассмотрим оператор 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
, которая проверяет переданный аргумент на следующие условия:
- аргумент является строкой
- длина строки меньше или равна 8
- длина строки больше или равна 2
Примеры работы функции:
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}
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.