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
也支持守卫。
我需要一个具有某种分步逻辑的函数,我想知道如何制作一个。以某站点的登录流程为例,需要如下逻辑:
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
也支持守卫。