2
votes

I've recently started to learn Elixir and I'm really quite enjoying it, but it's the first functional programming language I've ever used. The problem I'm faced with is from what I've been reading through tutorials and watching screencasts at LearnElixir is that you should try to avoid using IF type statements.

But I find myself constantly nesting cond or case

I would solve these solutions in other languages like Golang or Javascript by just using an if statment with an early return, so that the code beyond that would not run, this prevented me 99% of the time from having to nest conditionals by just checking falsy values and returning.

So in Elixir (or other functional programming languages) how would you go about writing something like the below in a proper way without using nesting, and taking advantage of the functionality of the language.

def loginPost(conn, %{"user" => user_params}) do
    # Look for user in database
    query = from u in User,
      where: u.email == ^user_params["email"],
      select: [u.email, u.username, u.password]

    data = Repo.all(query)

    # Check to see if a user had been found
    case (length data) == 0 do
      true -> # No user was found, send an error message
        conn
        |> json(%{ success: false, errors: ["Wrong username or password"]})
      false -> # A user was found, compare password
        [[email, username, db_password]] = data
        case Comeonin.Bcrypt.checkpw(user_params["password"], db_password) do
          true -> # Password was correct, set session and return a json response
            conn
            |> put_session(:authenticated, true)
            |> put_session(:username, username)
            |> put_session(:email, email)
            |> json(%{success: true}) # Send json response and redirect on client side
          false -> # Password was incorrect, send an error message
            conn
            |> json(%{success: false, errors: ["Wrong username or password"]})
        end
    end
  end
end
2
i am more curious about the reason why if should be avoided... it just simply doesn't make sense. in any languages, branching is the building block of a program. there must be some branching occuring somewhere in a nontrivial program.Jason Hu
@HuStmpHrrr A few people I've talked to on Elixir's IRC / Slack usually say things like, oh if I get too nested I figure I'm trying to do too much in one function and will just write another oneDatsik
that's something else. it applies in all languages, i.e. to avoid too nested structures. in fp, there isn't loop structure, so the critics will all point to branching. i see the point. it's more about modularity. not just to avoid some specific structure for some reason. i think this code will be better if if is used.Jason Hu
@HuStmpHrrr By Code do you mean the given example above?Datsik
yes. pattern matching on boolean is weird. but matching a group of boolean is a common (and good) practice.Jason Hu

2 Answers

7
votes

One way to go is to use with. You could create separate functions which could look like this:

def authenticate(email, password) do
  with {:ok, user} <- find_user(email),
    {:ok, user} <- validate_password(user, password),
    {:ok, user} <- validate_preconditions(user)
  do: {:ok, user}
end

defp find_user(email) do
  # return {:ok, user} if user is found, return {:error, :user_not_found} otherwise
end

defp validate_password(user, password) do
  # return {:ok, user} if password is correct, return {:error, :invalid_password} otherwise
end

defp validate_preconditions(user) do
  # return {:ok, user} if user is not banned or whatever, return {:error, :cant_be_logged_in} otherwise
end

and then you could use it in your controller function like this:

def loginPost(conn, %{"user" => user_params}) do
  case authenticate(user_params["email"], user_params["password"]) do
    {:ok, user} -> # start session
    {:error, error_type} -> # handle error
  end
end

The example could be better but you get the point.

Also you could read the answers from this question

0
votes

One of the benefits (and constraints) of functional programming is that all functions must return a value. In typed FP (e. g. F#, Scala, Haskell) the return types of all possible exits to the function must be the same. Hence I can't have a function that returns false if the input is bad but returns a number if the input is ok. How to handle this situation?

1.) Make the return type of the function a tuple of some sort. This is a common practice in many languages. Erlang has lots of functions that return {:ok, value} when things work right and {:error, message} (or something quite similar) when there's an issue. Then the calling function interrogates the atom to insure it's :ok before using the second element in the tuple. This is a little bit hacky but it's not the worst thing in the world.

2.) You could throw an exception. Granted this seems a bit extreme there's no particularly good reason to avoid this practice if it makes sense.

3.) You can add guards on the input to insure you don't get bad inputs in the first place.

For example, consider this:

def max(a, b) when is_number(a) and is_number(b) do

This, of course, prevents me from accidentally calling max with a letter or really anything other than a number. One could further constrain the inputs with additional guards. This would, again, remove one of the reasons for early exit.

I offer these as three other approaches to the issue. I think @JustMichael's suggestion about using the with construct is also a great idea; I just add these approaches for sake of completeness.