Elixir 中的返回语句

Posted

技术标签:

【中文标题】Elixir 中的返回语句【英文标题】:Return statement in Elixir 【发布时间】:2015-11-21 06:39:55 【问题描述】:

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

1) 电子邮件是否存在?是 -> 继续;否 -> 返回错误

2) 电子邮件至少有 5 个字符?是 -> 继续;否 -> 返回错误

3) 密码是否存在?是 -> 继续;否 - 返回错误

等等……

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

【问题讨论】:

【参考方案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 的上下文中,您可以将您的检查声明为 a series of plugs and halt on first error。

【讨论】:

确认。我应该先阅读您的答案@sasajuric。我只是基本上说了你所做的同样的事情,只是没有那么雄辩。我会留下我的答案,即使它有点多余,因为铁路编程讨论很有趣。很高兴看到我和像你一样聪明的人的想法是一样的。 :) @OnorioCatenacci 您答案中的链接很有用,并增加了很多价值。此外,您的代码没有那么神奇,因此它以更简单的方式解释了这个问题。所以它肯定会为这个讨论增加价值:-)【参考方案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

【讨论】:

是的,我用with 实现了它:)【参考方案3】:

您正在寻找的是我所说的“提前退出”。很久以前,当我开始使用 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?("\x0000abcdefg")
** (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。也值得一读。

【讨论】:

【参考方案4】:

这是使用 Result(或 Maybe)monad 的理想场所!

目前有 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

【讨论】:

我考虑在这种情况下提到 Maybe monad,但因为我没有任何好的示例代码而绕过它。很高兴你提出来。【参考方案5】:

这正是我使用 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

注意,这会产生很多次长管道,而且很容易上瘾 :-)

管道库比我上面使用的模式匹配有更多的方法来保持管道。在示例和测试中查看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

此时,您的所有功能都没有相互连接。如果您正确分离了所有逻辑,则可以为每个函数添加一个 arity,以便在出现问题时将错误结果传播到调用堆栈。

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,所以写了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

还支持守卫。

【讨论】:

【参考方案8】:

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

【讨论】:

这可能有点棘手,因为在用户定义函数的末尾使用诸如“Logger.debug”之类的函数会使其返回 :ok 原子。 我知道所有这些,但问题是我并不总是希望将函数执行到最后一条语句。我可以轻松使用嵌套条件,这不是问题,我只是想确保没有更好的方法

以上是关于Elixir 中的返回语句的主要内容,如果未能解决你的问题,请参考以下文章

ETS 创造回报值

Phoenix/Elixir - 协议 Enumerable 未实现

Elixir/Phoenix 日期从工作日 + 周数

在 elixir 应用程序中访问项目版本

elixir东游记:实现一个简单的中文语句解析

对只运行一次的程序使用 elixir 应用程序