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?
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:
state
en entrée et renvoie {:ok, new_state}
ou {:error, reason}
.{: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 .
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
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.
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
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.
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.
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.