Return 长生不老药中的声明

Return statement in Elixir

我需要一个具有某种分步逻辑的函数,我想知道如何制作一个。以某站点的登录流程为例,需要如下逻辑:

1) Email is present? Yes -> Go on; No -> Return an error

2) Email has at least 5 characters? Yes -> Go on; No -> Return an error

3) Password is present? Yes -> Go on; No - Return an error

And so on ...

为了实现这一点,我通常会使用 return 语句,这样如果电子邮件不存在,我就会停止执行该函数并使其成为 return 错误。但是我在 Elixir 中找不到类似的东西所以我需要一个建议。我现在能看到的唯一方法是使用嵌套条件,但也许有更好的方法?

您不需要任何 return 语句,因为控制流操作 (case/conf/if…) 编辑的最后一个值 return 是函数的 return 值.检查 this part of the tutorial。我认为 cond do 是您在这种情况下需要的运算符。

这是一个有趣的问题,因为你需要执行多次检查,提前退出,并在这个过程中转换一些状态(连接)。我通常按​​以下方式处理此问题:

  • 我将每个检查实现为一个函数,它将 state 作为输入,returns {:ok, new_state}{:error, reason}.
  • 然后,我构建了一个将调用检查函数列表的通用函数,并且 return 第一个遇到的 {: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

你要找的就是我所说的 "early exit"。很久以前,当我开始使用 F# 进行函数式编程时,我遇到了同样的问题。我得到的答案可能具有指导意义:

Multiple Exits From F# Function

这也是对问题的一个很好的讨论(虽然又是 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)> 

当然,您会想要构建自己的密码测试,但一般原则仍然适用。


编辑:Zohaib Rauf 就这个想法做了 very extensive blog post。也值得一读。

这是使用 Result(或 Maybe)monad 的完美场所!

目前有 MonadEx and (shameless self-promotion) Towel 提供您需要的支持。

有了 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

这正是我使用 elixir 管道库的情况

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

请注意,这会多次产生长管道,而且会让人上瘾 :-)

Pipes 库比我上面使用的模式匹配有更多的方法来保持管道。查看示例和测试elixir-pipes

此外,如果验证成为您代码中的常见主题,也许是时候检查 Ecto 的变更集验证或 Vex 另一个除了验证您的输入什么都不做的库。

这是我发现的最简单的方法,无需借助匿名函数和复杂的代码。

您打算链接和退出的方法需要有一个特殊的参数来接受 {:error, _} 的元组。假设您有一些函数 return {: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

此时,您的 none 个函数已相互连接。如果您已正确分离所有逻辑,则可以向这些函数中的每一个添加一个元数,以便在出现问题时将错误结果传播到调用堆栈中。

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

虽然您最终可能会为每个函数创建一些额外的代码,但它非常易于阅读,并且您可以像使用匿名函数解决方案一样交替处理其中的大部分代码。不幸的是,如果不对函数的工作方式进行一些修改,您将无法从管道中通过它们传递数据。只是我的两分钱。祝你好运。

我知道这个问题很老了,但我 运行 遇到了同样的情况,发现从 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

太怀念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

也支持守卫。