web-dev-qa-db-ja.com

Elixirのステートメントを返す

なんらかの段階的なロジックを備えた関数が必要です。どうすれば作成できるのでしょうか。例として、サイトでのログインプロセスを考えてみましょう。次のロジックが必要です。

1)メールはありますか?はい->続けます。いいえ->エラーを返します

2)電子メールは5文字以上ですか?はい->続けます。いいえ->エラーを返します

3)パスワードはありますか?はい->続けます。いいえ-エラーを返します

等々 ...

これを実装するには、通常、returnステートメントを使用して、Eメールが存在しない場合に関数の実行を中止し、エラーを返すようにします。しかし、エリクサーでこれに似たものを見つけることができないので、アドバイスが必要です。今私が見ることができる唯一の方法はネストされた条件を使用することですが、多分もっと良い方法がありますか?

30
NoDisplayName

複数のチェックを実行し、早期に終了し、その過程でいくつかの状態(接続)を変換する必要があるため、これは興味深い問題です。私は通常、次のようにこの問題に取り組みます。

  • 各チェックを、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のコンテキストでは、チェックを 一連のプラグと最初のエラーで停止 として宣言できます。

27
sasajuric

私はこの質問が古いことを知っていますが、同じ状況に遭遇し、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
18
ewH

あなたが探しているのは、私が「初期出口」と呼ぶものです。かなり前にF#で関数型プログラミングを開始したときにも同じ質問がありました。それについて私が得た答えは有益かもしれません:

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は 非常に広範なブログ投稿 をこのアイデアについてだけ行いました。読む価値もあります。

6

これは、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
3
knrz

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

ガードもサポートされています。

2
Aetherus

これがまさにエリクサーパイプライブラリを使用する状況です

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
ash

これが、匿名関数や複雑なコードに頼らずに見つけた最も単純なアプローチです。

チェーンして終了するメソッドには、{: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セント。幸運を祈ります。

2
dimiguel