I need a function with some kind of a step-by-step logic and I wonder how I can make one. Let\'s take a log in process on a site as an example, so I need the following logic
This is an interesting problem because you need to perform multiple checks, exit early, and in the process transform some state (connection). I typically approach this problem as follows:
state
as an input and returns {:ok, new_state}
or {:error, reason}
.{:error, reason}
or {:ok, last_returned_state}
if all checks succeeded.Let's see the generic function first:
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
Now, we can use it as follows:
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
Or alternatively move all checks to named private functions and then do:
perform_checks(conn, [
&check_mail_presence/1,
&check_mail_format/1,
...
])
You could also look into the elixir-pipes which might help you express this with pipeline.
Finally, in the context of Phoenix/Plug, you could declare your checks as a series of plugs and halt on first error.
You don't need any return
statement, as the last value returned by a control flow operation (case/conf/if…) is the function's return value. Check this part of the tutorial. I think cond do
is the operator you need in this case.
I missed return
so much that I wrote a hex package called return.
The repository is hosted at https://github.com/Aetherus/return.
Here is the source code for 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
The macros can be used like
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
Guards are also supported.
This is the perfect place to use the Result (or Maybe) monad!
There's currently the MonadEx and (shameless self-promotion) Towel that provide the support you need.
With Towel, you could write:
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
And then, in your controller:
result = user |> has_email? |> valid_email? |> has_password? ...
case result do
{:ok, user} ->
# do stuff
{:error, reason} ->
# do other stuff
end
Here's the simplest approach that I've found without resorting to anonymous functions and complicated code.
Your methods that you intend to chain and exit from need to have a special arity that accepts a tuple of {:error, _}
. Let's assume you have some functions that return a tuple of either {:ok, _}
or {: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
At this point, none of your functions are connected to each other. If you've separated all of your logic correctly, you can add an arity to each of these functions to propogate errors results up the call stack if something were to go wrong.
def find(piped, username) do
case piped do
{:error, _} -> piped
_ -> find(username)
end
end
# repeat for your other two functions
Now all of your functions will properly propagate their errors up the call stack, and you can pipe them in your caller without worrying about whether or not they are transferring invalid state to the next method.
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
While you may end up creating some extra code for each of your functions, it's very easy to read and you can pipe through most of them interchangeably like you would with the anonymous function solution. Unfortunately, you won't be able to pass data through them from a pipe without some modification to how your functions work. Just my two cents. Best of luck.
I know this question is old, but I ran into this same situation and found that as of Elixir 1.2, you can also use the with statement that makes your code very readable. The do:
block will be executed if all clauses match, otherwise it will be halted and the non-matching value will be returned.
Example
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