2015-08-27 2 views
25

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

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

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

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

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

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

ответ

26

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

  • я реализовать каждый чек в виде функции, которая принимает 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 вы можете объявить свои чеки как a series of plugs and halt on first error.

+1

Ack. Я должен был сначала прочитать ваш ответ @sasajuric. Я просто в основном сказал то же самое, что вы делали не совсем так красноречиво. Я оставлю свой ответ, хотя это немного лишнее, потому что дискуссия по железнодорожному программированию интересна. Приятно видеть, что я размышляю так же, как кто-то такой же умный, как ты. :) –

+1

@OnorioCatenacci Ссылки в вашем ответе полезны и добавляют большую ценность. Кроме того, ваш код менее волшебный, поэтому он объясняет это проще. Так что это определенно добавляет ценность для этой дискуссии :-) – sasajuric

-2

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

+0

И это может быть немного сложно, потому что использование функций типа «Logger.debug» в конце пользовательской функции заставляет его возвращаться с помощью атома ': ok'. – Uniaika

+0

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

6

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

Multiple Exits From F# Function

Это также хорошее обсуждение вопроса (хотя опять же это 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 Рауф сделал very extensive blog post только на этой идее. Ну стоит читать.

3

Это идеальное место для использования Монады Результат (или Может быть)!

В настоящее время 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 
+0

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

2

Это как раз та ситуация, я бы использовать эликсир трубы библиотеки

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 другую библиотеку, которая ничего не делает, кроме проверки вашего ввода.

2

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

Ваши методы, которые вы собираетесь цеплять и выходить из, должны иметь особую сущность, которая принимает кортеж {: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 

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

16

Я знаю, что этот вопрос старый, но я столкнулся с этой ситуацией и обнаружил, что с 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 
+1

Да, я реализовал его с 'with' :) – JustMichael

1

return я пропустил так много, что я написал a hex package called 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 

гвардейской также поддерживаются.

Смежные вопросы