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}Полезное
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}Полезное
Ваше упражнение проверяется по этим тестам
defmodule Test do
use ExUnit.Case
describe "validate work" do
test "with valid data" do
assert Solution.validate("some") == {:ok, "some"}
assert Solution.validate("hello!!") == {:ok, "hello!!"}
end
test "with invalid data" do
assert Solution.validate(1) == {:error, :not_binary}
assert Solution.validate("a") == {:error, :too_short}
assert Solution.validate("hello, world!") == {:error, :too_long}
end
end
endРешение учителя откроется через:
20:00
