Бесплатный курс по Elixir. Зарегистрируйтесь для отслеживания прогресса →

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}
Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.

Полезное


Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе