なんらかの段階的なロジックを備えた関数が必要です。どうすれば作成できるのでしょうか。例として、サイトでのログインプロセスを考えてみましょう。次のロジックが必要です。
1)メールはありますか?はい->続けます。いいえ->エラーを返します
2)電子メールは5文字以上ですか?はい->続けます。いいえ->エラーを返します
3)パスワードはありますか?はい->続けます。いいえ-エラーを返します
等々 ...
これを実装するには、通常、return
ステートメントを使用して、Eメールが存在しない場合に関数の実行を中止し、エラーを返すようにします。しかし、エリクサーでこれに似たものを見つけることができないので、アドバイスが必要です。今私が見ることができる唯一の方法はネストされた条件を使用することですが、多分もっと良い方法がありますか?
複数のチェックを実行し、早期に終了し、その過程でいくつかの状態(接続)を変換する必要があるため、これは興味深い問題です。私は通常、次のようにこの問題に取り組みます。
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のコンテキストでは、チェックを 一連のプラグと最初のエラーで停止 として宣言できます。
私はこの質問が古いことを知っていますが、同じ状況に遭遇し、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
あなたが探しているのは、私が「初期出口」と呼ぶものです。かなり前にF#で関数型プログラミングを開始したときにも同じ質問がありました。それについて私が得た答えは有益かもしれません:
これも質問の良い議論です(ただし、これはF#です)。
http://fsharpforfunandprofit.com/posts/recipe-part2/
TL; DRは、atomのタプルとチェックするパスワード文字列を受け取り、返す一連の関数として関数を構築します。atomは、 :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は 非常に広範なブログ投稿 をこのアイデアについてだけ行いました。読む価値もあります。
これは、Result(または多分)モナドを使用するのに最適な場所です!
現在、必要なサポートを提供する MonadEx と(恥知らずな自己宣伝) タオル があります。
タオルで、あなたは書くことができます:
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
return
を逃してしまったので returnという16進パッケージ と書きました。
リポジトリは 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
ガードもサポートされています。
これがまさにエリクサーパイプライブラリを使用する状況です
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 入力を検証する以外に何もしない別のライブラリを確認するときがきたかもしれません。
これが、匿名関数や複雑なコードに頼らずに見つけた最も単純なアプローチです。
チェーンして終了するメソッドには、{: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
関数ごとにいくつかの追加コードを作成することになるかもしれませんが、それは非常に読みやすく、匿名関数ソリューションの場合と同じように、それらのほとんどを交換可能にパイプ処理できます。残念ながら、関数の動作方法に変更を加えないと、パイプからデータを渡すことができません。ちょうど私の2セント。幸運を祈ります。