NoDisplayName
NoDisplayName

Reputation: 15736

Return statement in Elixir

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:

1) Email is present? Yes -> Go on; No -> Return an error

2) Email has at least 5 characters? Yes -> Go on; No -> Return an error

3) Password is present? Yes -> Go on; No - Return an error

And so on ...

And to implement this, I would usually use a return statement so that if the email is not present, I quit executing the function and make it return an error. But I can't find something similar to this in Elixir so I need an advice. The only way I can see now is to use nested conditions but maybe there is a better way?

Upvotes: 33

Views: 12830

Answers (8)

ewH
ewH

Reputation: 2683

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

Upvotes: 22

Aetherus
Aetherus

Reputation: 8898

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.

Upvotes: 2

dimiguel
dimiguel

Reputation: 1569

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.

Upvotes: 2

Onorio Catenacci
Onorio Catenacci

Reputation: 15303

What you're looking for is what I'd call an "early exit". I had the same question when I started with functional programming in F# quite a while ago. The answers I got for that may be instructive:

Multiple Exits From F# Function

This is also a good discussion of the question (although again it's F#):

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

TL;DR construct your functions as a series of functions each taking and returning a tuple of an atom and the password string to check. The atom will either be :ok or :error. Like so:

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

And you would use it like so:

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

Of course, you'll want to construct your own password tests but the general principle should still apply.


EDIT: Zohaib Rauf did a very extensive blog post on just this idea. Well worth reading as well.

Upvotes: 7

ash
ash

Reputation: 711

This is exactly the situation I'd use elixir pipes library

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

Note, this produces long pipes a lot of times and it is addictive :-)

Pipes library has more ways to keep piping than pattern matching I used above. Have a look elixir-pipes at the examples and tests.

Also, if validation becomes a common theme in your code maybe it is time to check Ecto's changeset validations or Vex another library that does nothing else but validate your input.

Upvotes: 2

knrz
knrz

Reputation: 1811

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

Upvotes: 3

sasajuric
sasajuric

Reputation: 6059

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:

  • I implement each check as a function which takes state as an input and returns {:ok, new_state} or {:error, reason}.
  • Then, I build a generic function that will invoke a list of check functions, and return either the first encountered {: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.

Upvotes: 31

H&#233;cate
H&#233;cate

Reputation: 1035

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.

Upvotes: -2

Related Questions