Операция возврата в Elixir
Мне нужна функция с какой-то пошаговой логикой, и мне интересно, как я могу ее создать. В качестве примера возьмем процесс регистрации на сайте, поэтому мне нужна следующая логика:
1) Электронная почта присутствует? Да → Продолжайте; Нет → Возврат ошибки
2) Электронная почта имеет не менее 5 символов? Да → Продолжайте; Нет → Возврат ошибки
3) Пароль присутствует? Да → Продолжайте; Нет - вернуть ошибку
И так далее...
И для реализации этого я обычно использовал бы оператор return
, чтобы, если письмо отсутствует, я прекратил выполнение функции и возвратил ее. Но я не могу найти что-то подобное этому в Эликсире, поэтому мне нужен совет. Единственный способ, который я вижу сейчас, - использовать вложенные условия, но, возможно, есть лучший способ?
Ответы
Ответ 1
Это интересная проблема, потому что вам нужно выполнить несколько проверок, выйти рано и в процессе преобразовать какое-то состояние (соединение). Я обычно подхожу к этой проблеме следующим образом:
- Я реализую каждую проверку как функцию, которая принимает
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-pipes, который может помочь вам выразить это с помощью конвейера.
Наконец, в контексте Phoenix/Plug вы можете объявить свои чеки как серию подключаемых модулей и остановить первую ошибку.
Ответ 2
Я знаю, что этот вопрос старый, но я столкнулся с этой ситуацией и обнаружил, что с Elixir 1.2 вы также можете использовать with
, который делает ваш код очень читаемым. Блок 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
Ответ 3
То, что вы ищете, - это то, что я назвал бы "ранним выходом". У меня был тот же вопрос, когда я начал с функционального программирования в F # совсем недавно. Ответы, которые я получил для этого, могут быть поучительными:
Несколько выходов из функции F #
Это также хорошее обсуждение вопроса (хотя снова это F #):
http://fsharpforfunandprofit.com/posts/recipe-part2/
TL; DR создайте свои функции как ряд функций, каждый из которых принимает и возвращает кортеж атома и строку пароля для проверки. Атом будет либо: ok, либо: ошибка. Например:
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)>
Конечно, вы захотите создать свои собственные тесты паролей, но общий принцип все равно должен применяться.
EDIT: Zohaib Rauf сделал очень обширное сообщение в блоге только по этой идее. Также стоит прочитать.
Ответ 4
Это идеальное место для использования монады Результат (или Может быть)!
В настоящее время 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
Ответ 5
Это именно та ситуация, в которой я бы использовал библиотеку elixir pipes
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-pipes в примерах и тестах.
Кроме того, если валидация становится общей темой в вашем коде, возможно, пришло время проверить проверки изменений Ecto или Vex другую библиотеку, которая делает ничего, кроме подтверждения вашего ввода.
Ответ 6
Здесь самый простой подход, который я нашел, не прибегая к анонимным функциям и сложному коду.
Ваши методы, которые вы намерены цепочки и выйти из, должны иметь особую ясность, которая принимает кортеж {: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
Пока вы можете создать дополнительный код для каждой из ваших функций, он очень прост в чтении, и вы можете использовать большинство из них в качестве взаимозаменяемого, как с анонимным решением. К сожалению, вы не сможете передавать данные через них из канала без каких-либо изменений в работе ваших функций. Только мои два цента. Удачи.
Ответ 7
Я пропустил 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
Защиты также поддерживаются.
Ответ 8
Вам не нужен оператор return
, поскольку последнее значение, возвращаемое операцией потока управления (case/conf/if...), является возвращаемым значением функции. Проверьте эту часть учебника. Я думаю, что cond do
- это оператор, который вам нужен в этом случае.