web-dev-qa-db-fra.com

Déclaration de retour dans Elixir

J'ai besoin d'une fonction avec une sorte de logique pas à pas et je me demande comment je peux en créer une. Prenons un processus de connexion sur un site comme exemple, j'ai donc besoin de la logique suivante:

1) L'email est présent? Oui -> Continuez; Non -> Retourne une erreur

2) L'email contient au moins 5 caractères? Oui -> Continuez; Non -> Retourne une erreur

3) Le mot de passe est présent? Oui -> Continuez; Non - Retourne une erreur

Etc ...

Et pour implémenter cela, j'utilise généralement une instruction return de sorte que si l'e-mail n'est pas présent, je quitte l'exécution de la fonction et lui fais retourner une erreur. Mais je ne trouve pas quelque chose de similaire à Elixir, j'ai donc besoin d'un conseil. La seule façon dont je peux voir maintenant est d'utiliser des conditions imbriquées mais peut-être y a-t-il une meilleure façon?

30
NoDisplayName

Il s'agit d'un problème intéressant car vous devez effectuer plusieurs vérifications, quitter tôt et, dans le processus, transformer un état (connexion). J'aborde généralement ce problème comme suit:

  • J'implémente chaque vérification comme une fonction qui prend state en entrée et renvoie {:ok, new_state} ou {:error, reason}.
  • Ensuite, je construis une fonction générique qui invoquera une liste de fonctions de vérification et renverra soit la première {:error, reason} ou {:ok, last_returned_state} si toutes les vérifications ont réussi.

Voyons d'abord la fonction générique:

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

Maintenant, nous pouvons l'utiliser comme suit:

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

Ou déplacez toutes les vérifications vers des fonctions privées nommées, puis procédez comme suit:

perform_checks(conn, [
  &check_mail_presence/1,
  &check_mail_format/1,
  ...
])

Vous pouvez également examiner les elixir-pipes qui pourraient vous aider à exprimer cela avec le pipeline.

Enfin, dans le contexte de Phoenix/Plug, vous pouvez déclarer vos chèques comme ne série de plugs et s'arrêter à la première erreur .

27
sasajuric

Je sais que cette question est ancienne, mais j'ai rencontré cette même situation et j'ai découvert qu'à partir d'Elixir 1.2, vous pouvez également utiliser l'instruction with qui rend votre code très lisible. Le do: le bloc sera exécuté si toutes les clauses correspondent, sinon il sera interrompu et la valeur non correspondante sera retournée.

Exemple

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

Ce que vous cherchez, c'est ce que j'appellerais une "sortie anticipée". J'avais la même question quand j'ai commencé avec la programmation fonctionnelle en F # il y a un bon moment. Les réponses que j'ai obtenues à ce sujet peuvent être instructives:

Sorties multiples de la fonction F #

C'est aussi une bonne discussion de la question (même si c'est encore F #):

http://fsharpforfunandprofit.com/posts/recipe-part2/

TL; DR construisent vos fonctions comme une série de fonctions prenant et retournant chacune un Tuple d'un atom et la chaîne de mot de passe à vérifier. Le atom atom sera soit : ok ou: erreur. Comme ceci:

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

Et vous l'utiliseriez comme ceci:

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)> 

Bien sûr, vous voudrez construire vos propres tests de mot de passe, mais le principe général devrait toujours s'appliquer.


EDIT: Zohaib Rauf a fait un article de blog très complet sur cette idée. Il vaut également la peine d'être lu.

6
Onorio Catenacci

C'est l'endroit idéal pour utiliser la monade Résultat (ou Peut-être)!

Il y a actuellement MonadEx et (auto-promotion éhontée) Serviette qui fournissent le soutien dont vous avez besoin.

Avec Towel, vous pouvez écrire:

  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

Et puis, dans votre contrôleur:

result = user |> has_email? |> valid_email? |> has_password? ...
case result do
  {:ok, user} ->
    # do stuff
  {:error, reason} ->
    # do other stuff
end
3
knrz

J'ai tellement manqué return que j'ai écrit n paquet hexadécimal appelé return .

Le référentiel est hébergé sur https://github.com/Aetherus/return .

Voici le code source de la 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

Les macros peuvent être utilisées comme

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

Les gardes sont également pris en charge.

2
Aetherus

C'est exactement la situation que j'utiliserais avec la bibliothèque de tuyaux d'élixir

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

Remarque, cela produit souvent de longs tuyaux et cela crée une dépendance :-)

La bibliothèque de tuyaux a plus de façons de conserver la tuyauterie que la correspondance de motifs que j'ai utilisée ci-dessus. Jetez un oeil tuyaux d'élixir aux exemples et tests.

De plus, si la validation devient un thème commun dans votre code, il est peut-être temps de vérifier les validations de l'ensemble de modifications d'Ecto ou Vex une autre bibliothèque qui ne fait rien d'autre que valider votre entrée.

2
ash

Voici l'approche la plus simple que j'ai trouvée sans avoir recours à des fonctions anonymes et à du code compliqué.

Vos méthodes que vous souhaitez chaîner et quitter doivent avoir une arité spéciale qui accepte un tuple de {:error, _}. Supposons que certaines fonctions renvoient un tuple de {:ok, _} ou {: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

À ce stade, aucune de vos fonctions n'est connectée les unes aux autres. Si vous avez correctement séparé toute votre logique, vous pouvez ajouter une arité à chacune de ces fonctions pour propager les résultats des erreurs dans la pile des appels si quelque chose devait mal se passer.

def find(piped, username) do
   case piped do
     {:error, _} -> piped
     _           -> find(username)
   end
end

# repeat for your other two functions

Maintenant, toutes vos fonctions propageront correctement leurs erreurs dans la pile d'appels, et vous pouvez les diriger dans votre appelant sans vous soucier de savoir si elles transfèrent ou non un état non valide à la méthode suivante.

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

Bien que vous puissiez finir par créer du code supplémentaire pour chacune de vos fonctions, il est très facile à lire et vous pouvez les parcourir de manière interchangeable comme vous le feriez avec la solution de fonction anonyme. Malheureusement, vous ne pourrez pas passer de données à travers eux à partir d'un canal sans quelques modifications du fonctionnement de vos fonctions. Juste mes deux cents. Bonne chance.

2
dimiguel