Заявление о возврате в Эликсире

Мне нужна функция с какой-то пошаговой логикой, и мне интересно, как я могу ее сделать. В качестве примера возьмем процесс входа в систему на сайте, поэтому мне понадобится следующая логика:

1) Электронная почта присутствует? Да -> Продолжайте; Нет -> Вернуть ошибку

2) Электронная почта должна содержать не менее 5 символов? Да -> Продолжайте; Нет -> Вернуть ошибку

3) Пароль присутствует? Да -> Продолжайте; Нет - вернуть ошибку

И так далее ...

И для реализации этого я обычно использовал оператор return, чтобы, если электронное письмо отсутствует, я прекращал выполнение функции и заставлял ее возвращать ошибку. Но я не могу найти что-то подобное в Эликсире, поэтому мне нужен совет. Единственный способ, который я сейчас вижу, - это использовать вложенные условия, но, может быть, есть способ лучше?


person NoDisplayName    schedule 27.08.2015    source источник


Ответы (8)


Это интересная проблема, потому что вам нужно выполнить несколько проверок, выйти раньше и в процессе преобразовать какое-то состояние (соединение). Обычно я подхожу к этой проблеме следующим образом:

  • Я реализую каждую проверку как функцию, которая принимает state в качестве входных данных и возвращает {:ok, new_state} или {:error, reason}.
  • Затем я создаю универсальную функцию, которая будет вызывать список проверочных функций и возвращать либо первое встреченное {:error, reason}, либо {:ok, last_returned_state}, если все проверки выполнены успешно.

Давайте сначала посмотрим на общую функцию:

defp perform_checks(state, []), do: {:ok, state}
defp perform_checks(state, [check_fun | remaining_checks]) do
  case check_fun.(state) do
    {:ok, new_state} -> perform_checks(new_state, remaining_checks)
    {:error, _} = error -> error
  end
end

Теперь мы можем использовать его следующим образом:

perform_checks(conn, [
  # validate mail presence
  fn(conn) -> if (...), do: {:error, "Invalid mail"}, else: {:ok, new_conn} end,

  # validate mail format
  fn(conn) -> if (...), do: {:error, "Invalid mail"}, else: {:ok, new_conn} end,

  ...
])
|> case do
  {:ok, state} -> do_something_with_state(...)
  {:error, reason} -> do_something_with_error(...)
end

Или, в качестве альтернативы, переместите все проверки в именованные частные функции, а затем выполните:

perform_checks(conn, [
  &check_mail_presence/1,
  &check_mail_format/1,
  ...
])

Вы также можете изучить elixir-pipe, которые могут помочь вам выразить это с помощью конвейера.

Наконец, в контексте Phoenix / Plug вы можете объявить свои чеки как серия откатов и остановка при первой ошибке.

person sasajuric    schedule 27.08.2015
comment
Ack. Я должен был сначала прочитать ваш ответ @sasajuric. Я просто сказал то же самое, что и вы, только не так красноречиво. Я оставлю свой ответ, хотя он немного избыточен, потому что обсуждение железнодорожного программирования - интересный. Приятно видеть, что я мыслю в том же духе, что и такой умный человек, как ты. :) - person Onorio Catenacci; 27.08.2015
comment
@OnorioCatenacci Ссылки в вашем ответе полезны и добавляют большую ценность. Кроме того, ваш код менее волшебный, поэтому он объясняет проблему проще. Так что это определенно добавляет ценности этому обсуждению :-) - person sasajuric; 27.08.2015

Я знаю, что это старый вопрос, но я столкнулся с той же ситуацией и обнаружил, что начиная с Elixir 1.2 вы также можете использовать _ 1_, который делает ваш код очень удобочитаемым. Блок do: будет выполнен, если все предложения совпадают, в противном случае он будет остановлен и будет возвращено несоответствующее значение.

Пример

defmodule MyApp.UserController do
  use MyApp.Web, :controller

  def create(conn, params) do
    valid = 
      with {:ok} <- email_present?(params["email"]),
        {:ok} <- email_proper_length?(params["email"),
        {:ok} <- password_present?(params["password"]),
      do: {:ok} #or just do stuff here directly

    case valid do
      {:ok} -> do stuff and render ok response
      {:error, error} -> render error response
    end
  end

  defp email_present?(email) do
    case email do
      nil -> {:error, "Email is required"}
      _ -> {:ok}
    end
  end

  defp email_proper_length?(email) do
    cond do
      String.length(email) >= 5 -> {:ok}
      true -> {:error, "Email must be at least 5 characters"}
    end
  end

  defp password_present?(password) do
    case email do
      nil -> {:error, "Password is required"}
      _ -> {:ok}
    end
  end
end
person ewH    schedule 23.04.2016
comment
Ага, реализовал с with :) - person NoDisplayName; 23.04.2016

Вы ищете то, что я бы назвал «ранним выходом». У меня был тот же вопрос, когда я некоторое время назад начинал заниматься функциональным программированием на F #. Ответы, которые я получил на это, могут быть поучительными:

Множественные выходы из функции F #

Это тоже хорошее обсуждение вопроса (хотя опять же F #):

http://fsharpforfunandprofit.com/posts/recipe-part2/

TL; DR конструируйте свои функции как серию функций, каждая из которых принимает и возвращает кортеж атома и строку пароля для проверки. Атом будет либо: ok, либо: error. Вот так:

defmodule Password do

  defp password_long_enough?({:ok = a, p}) do
    if(String.length(p) > 6) do
      {:ok, p}
    else
      {:error,p}
    end
  end

  defp starts_with_letter?({:ok = a, p}) do
   if(String.printable?(String.first(p))) do
     {:ok, p}
   else
     {:error,p}
   end      
  end


  def password_valid?(p) do
    {:ok, _} = password_long_enough?({:ok,p}) |> starts_with_letter?
  end

end

И вы бы использовали это так:

iex(7)> Password.password_valid?("ties")
** (FunctionClauseError) no function clause matching in Password.starts_with_letter?/1
    so_test.exs:11: Password.starts_with_letter?({:error, "ties"})
    so_test.exs:21: Password.password_valid?/1
iex(7)> Password.password_valid?("tiesandsixletters")
{:ok, "tiesandsixletters"}
iex(8)> Password.password_valid?("\x{0000}abcdefg")
** (MatchError) no match of right hand side value: {:error, <<0, 97, 98, 99, 100, 101, 102, 103>>}
    so_test.exs:21: Password.password_valid?/1
iex(8)> 

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


РЕДАКТИРОВАТЬ: Зохайб Рауф сделал очень обширный пост в блоге именно по этой идее. Также стоит прочитать.

person Onorio Catenacci    schedule 27.08.2015

Это идеальное место для использования монады Result (или Maybe)!

В настоящее время существует MonadEx и (бессовестная самореклама) Towel, которые обеспечат вам необходимую поддержку.

Используя полотенце, вы можете написать:

  use Towel

  def has_email?(user) do
    bind(user, fn u ->
      # perform logic here and return {:ok, user} or {:error, reason}
    end)
  end

  def valid_email?(user) do
    bind(user, fn u ->
      # same thing
    end)
  end

  def has_password?(user) do
    bind(user, fn u ->
      # same thing
    end)
  end

А затем в вашем контроллере:

result = user |> has_email? |> valid_email? |> has_password? ...
case result do
  {:ok, user} ->
    # do stuff
  {:error, reason} ->
    # do other stuff
end
person knrz    schedule 28.08.2015
comment
Я подумал о том, чтобы упомянуть монаду Maybe в этом контексте, но обошел стороной, потому что у меня не было хорошего примера кода. Я рада, что вы подняли этот вопрос. - person Onorio Catenacci; 28.08.2015

Это именно та ситуация, в которой я бы использовал библиотеку elixir pipe

defmodule Module do
  use Phoenix.Controller
  use Pipe

  plug :action

  def action(conn, params) do
    start_val = {:ok, conn, params}
    pipe_matching {:ok, _, _},
      start_val
        |> email_present
        |> email_length
        |> do_action
  end

  defp do_action({_, conn, params}) do
    # do stuff with all input being valid
  end

  defp email_present({:ok, _conn, %{ "email" => _email }} = input) do
    input
  end
  defp email_present({:ok, conn, params}) do
    bad_request(conn, "email is a required field")
  end

  defp email_length({:ok, _conn, %{ "email" => email }} = input) do
    case String.length(email) > 5 do
      true -> input
      false -> bad_request(conn, "email field is too short")
  end

  defp bad_request(conn, msg) do
    conn 
      |> put_status(:bad_request) 
      |> json( %{ error: msg } )
  end
end

Обратите внимание, это часто приводит к образованию длинных трубок, и это вызывает привыкание :-)

В библиотеке каналов больше способов сохранить конвейер, чем сопоставление с образцом, которое я использовал выше. Посмотрите elixir-pipe на примеры и тесты.

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

person ash    schedule 28.08.2015

Вот самый простой подход, который я нашел, не прибегая к анонимным функциям и сложному коду.

Ваши методы, которые вы собираетесь объединить в цепочку и выйти из них, должны иметь особую арность, которая принимает кортеж {:error, _}. Предположим, у вас есть функции, возвращающие кортеж из {:ok, _} или {:error, _}.

# This needs to happen first
def find(username) do
  # Some validation logic here
  {:ok, account}
end

# This needs to happen second
def validate(account, params) do 
  # Some database logic here
  {:ok, children}
end

# This happens last
def upsert(account, params) do
  # Some account logic here
  {:ok, account}
end

На данный момент ни одна из ваших функций не связана друг с другом. Если вы правильно разделили всю свою логику, вы можете добавить арность к каждой из этих функций, чтобы распространять ошибки в стеке вызовов, если что-то пойдет не так.

def find(piped, username) do
   case piped do
     {:error, _} -> piped
     _           -> find(username)
   end
end

# repeat for your other two functions

Теперь все ваши функции будут правильно распространять свои ошибки по стеку вызовов, и вы можете передавать их в вызывающую программу, не беспокоясь о том, передают ли они недопустимое состояние следующему методу.

put "/" do 
  result = find(username)
    |> validate(conn.params)
    |> upsert(conn.params)

  case result do
    {:error, message} -> send_resp(conn, 400, message)
    {:ok, _}          -> send_resp(conn, 200, "")
  end
end

Хотя вы можете в конечном итоге создать дополнительный код для каждой из ваших функций, его очень легко читать, и вы можете передавать большинство из них взаимозаменяемо, как если бы вы делали это с решением анонимной функции. К сожалению, вы не сможете передавать данные через них из канала без некоторых изменений в работе ваших функций. Всего два цента. Удачи.

person dimiguel    schedule 07.12.2015

Я так return соскучился, что написал шестнадцатеричный пакет под названием return.

Репозиторий размещен по адресу https://github.com/Aetherus/return.

Вот исходный код для v0.0.1:

defmodule Return do
  defmacro func(signature, do: block) do
    quote do
      def unquote(signature) do
        try do
          unquote(block)
        catch
          {:return, value} -> value
        end
      end
    end
  end

  defmacro funcp(signature, do: block) do
    quote do
      defp unquote(signature) do
        try do
          unquote(block)
        catch
          {:return, value} -> value
        end
      end
    end
  end

  defmacro return(expr) do
    quote do
      throw {:return, unquote(expr)}
    end
  end

end

Макросы можно использовать как

defmodule MyModule do
  require Return
  import  Return

  # public function
  func x(p1, p2) do
    if p1 == p2, do: return 0
    # heavy logic here ...
  end

  # private function
  funcp a(b, c) do
    # you can use return here too
  end
end

Охранники также поддерживаются.

person Aetherus    schedule 04.02.2017

Вам не нужен какой-либо оператор return, поскольку последнее значение, возвращаемое операцией потока управления (case / conf / if…), является значением, возвращаемым функцией. Проверьте эту часть руководства. Я думаю, что cond do - именно тот оператор, который вам нужен в этом случае.

person Hécate    schedule 27.08.2015
comment
И это может быть немного сложно, потому что использование таких функций, как Logger.debug в конце пользовательской функции, заставляет ее возвращаться с атомом :ok. - person Hécate; 27.08.2015
comment
Я все это знаю, но дело в том, что я не всегда хочу выполнять функцию до последнего оператора. Я мог бы легко использовать вложенные условия, это не проблема, я просто хочу убедиться, что нет лучшего способа - person NoDisplayName; 27.08.2015