Return statement in Elixir

后端 未结 8 1379
野趣味
野趣味 2020-12-14 00:38

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

相关标签:
8条回答
  • 2020-12-14 01:13

    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.

    0 讨论(0)
  • 2020-12-14 01:13

    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.

    0 讨论(0)
  • 2020-12-14 01:16

    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.

    0 讨论(0)
  • 2020-12-14 01:22

    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
    
    0 讨论(0)
  • 2020-12-14 01:29

    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.

    0 讨论(0)
  • 2020-12-14 01:31

    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
    
    0 讨论(0)
提交回复
热议问题