DEV Community

loading...
Cover image for The Beauty Of [Functional Programming]

The Beauty Of [Functional Programming]

elixirprogrammer profile image Anthony Gonzalez Updated on ・2 min read

Programming languages that are not functional can get messy with lots of if-else statements especially if you are a beginner. With functional programming languages that practice is not followed, let's dive in with a simple example with the Elixir programming language.

defmodule Person do
    def can_drink(age) do
        if age != nil do
            cond do
                age < 18 ->
                    "Nope!"
                age < 21 ->
                    "Not in the US!"
                true ->
                "YES!!!"
            end
        else
            "You're not a Person!"
        end
    end
end

IO.puts Person.can_drink(age)
Enter fullscreen mode Exit fullscreen mode

In that example, the module Person will check the age that is passed into the can_drink(age) function for legal drinking age, and return a string, conditionals in elixir are a simpler way to create if-else statements but we can make it way better.

defmodule Person do
    def can_drink(age) do
        if age != nil do
            cond do
                age < 18 ->
                    "Nope!"
                age < 21 ->
                    "Not in the US!"
                true ->
                "YES!!!"
            end
        else
            "You're not a Person!"
        end
    end

    def can_drink_better(age) do
        legal(age)
    end
end

IO.puts Person.can_drink(age)
Enter fullscreen mode Exit fullscreen mode

We added the can_drink_better(age) function which calls a legal function, so now let's create the legal function.

defmodule Person do
    def can_drink(age) do
        if age != nil do
            cond do
                age < 18 ->
                    "Nope!"
                age < 21 ->
                    "Not in the US!"
                true ->
                "YES!!!"
            end
        else
            "You're not a Person!"
        end
    end

    def can_drink_better(age) do
        legal(age)
    end

    def legal(age) when is_nil(age), do: "You're not a Person"
    def legal(age) when age < 18, do: "Nope!"
    def legal(age) when age < 21, do: "Not in the US!"
    def legal(age) when age >= 21, do: "YES!!!"
end

IO.puts Person.can_drink_better(age)
Enter fullscreen mode Exit fullscreen mode

We created 4 legal(age) functions in the short inline form, the first checks if age is nil, it has to be the first function that it gets called, in elixir functions are called from top to bottom, because if it's placed at the bottom when age >= 21 will always be true due that nil it's not an integer.

It might look a little bit confusing if you come from another language, in Elixir you can call a function with the same name multiple times once until all the conditions are met, that's the beauty of functional programming. So now we can just remove the conditional function and be amazed by the awesome result.

defmodule Person do
    def can_drink(age) do
        legal(age)
    end

    def legal(age) when is_nil(age), do: "You're not a Person"
    def legal(age) when age < 18, do: "Nope!"
    def legal(age) when age < 21, do: "Not in the US!"
    def legal(age) when age >= 21, do: "YES!!!"
end

IO.puts Person.can_drink(age)
Enter fullscreen mode Exit fullscreen mode

Now our code looks cleaner than a baby's mind, functional programming with Elixir is an exquisite dish served in a 5 stars international restaurant.

Join The Elixir Army

Discussion (16)

pic
Editor guide
Collapse
lifelongthinker profile image
Sebastian

Programming languages that are not functional can get messy with lots of if-else statements especially if you are a beginner

Come on, any language can get messy if used improperly. This is a problem related to the user, not the language.

OOP and FP both have their merits, none is inherently better than the other. Let's agree to pick the right tool for the task at hand.

Collapse
andreidascalu profile image
Andrei Dascalu

Well, thing is OOP by its 4 pillars nudges you towards shooting yourself in the foot in the long run. All the attempts to fix the paradigm are actually borrowing from FP.

Ever since I dove into DDD and saw its reliance on immutability I've been failing to see any inherent merit of OOP (aside from having become historically dominant for a long time, mostly due to functional paradigms being fairly compute inneficient)

Collapse
elixirprogrammer profile image
Anthony Gonzalez Author

You're absolutely right, but with functional programming, it's not as easy, you need a completely different mindset and the principles force you to not be so careless or not so procedural.

Collapse
lifelongthinker profile image
Sebastian

This is the very same argument ("FP is inherently better, NON-FP is inherently messier,...") just in different clothing.

Yes, FP is different from NON-FP (not so "procedural", whatever that entails), but that's a trivial statement.

Again: There is nothing inherently messier or less messy in OOP/NON-FP than FP. Two different ways of solving problems, both with their own challenges and drawbacks.

Thread Thread
elixirprogrammer profile image
Anthony Gonzalez Author

I agree, I'm not saying that is better by any means, it's just another tool to get the job done. We all think differently, we can solve the same problems in multiple ways, it is always going to depend on what makes us feel comfortable in our brains.

Thread Thread
lifelongthinker profile image
Sebastian

Now we have an agreement 😅👍

Maybe we should do a series on how to solve typical problems in OOP and FP (say C# and Elixir) comparing the two?

Thread Thread
elixirprogrammer profile image
Anthony Gonzalez Author

Sure, that would be a really interesting series, I'm down!

Collapse
aminmansuri profile image
hidden_dude

Prolog solution is better:

legal_message(Age, "Nope!") :- Age < 18.
legal_message(Age, "Not in the US") :- Age < 21, Age >= 18.
legal_message(Age, "YES!!!") :- Age >= 21.

The nil message is not even necessary it's implicit.

My paradigm is better than yours I win. ;)

I can even have several results for a single case in Prolog. And it's more truly declarative than most FP languages.

Collapse
aminmansuri profile image
hidden_dude

But in all seriousness real OO is directly against case statements like these.

It intends to group decisions about the kind of thing you have in a single place.

So an OO way of doing this would be to compute an Age class on a person and then have all Age rules apply to that age class.

So you could ask a person what age classification they have like Senior, Adult, Child, etc.. and all things related to being an Adult or Child would center there.

This is overkill of course, but that would be the OO spirit.

In Java this can be approximated with the very non-OO enum operator that can make these sorts of minimal type classes quite easy to build.

But the idea of removing if-elses is so that your program isn't riddled with the same if-else all over the place but maybe in only on place.

Of course often a simple if-else is just simpler then some complex idiom like subscribing age values to behavior via callbacks or listeners.

Collapse
aminmansuri profile image
hidden_dude • Edited

But here's a modern Java example:

var legalMessage = new ImmutableRangeMap.<Integer, String>builder()
   .put(null, "You're not a Person")
   .put(lessThan(18), "Nope!!")
   .put(closedOpen(18,21), "Not in the US!!")
   .put(atLeast(21), "YES!!!")
   .build();

System.out.println(legalMessage.get(age));
Enter fullscreen mode Exit fullscreen mode

In OO it's all about whether you're using the right objects or not. Many times people get sloppy. (This data structure comes from the Guava library)

Thread Thread
elixirprogrammer profile image
Anthony Gonzalez Author

Nice one! Hard to believe that's Java. I agree, you can get sloppy with any language, even with Elixir, check this example:

def handle_event("upvote", %{"p_id" => post_id, "u_id" => user_id}, socket) do
    if user_id == "false" do
      {:noreply, redirect(socket, to: Routes.pow_session_path(socket, :new), replace: true)}
    else
      user = Repo.get!(User, user_id)
      post = Community.get_post!(post_id)
      vote = %Vote{}

      case current_user_voted?(user_id, post_id) do
        "" ->
          case Votes.create_vote(user, post, vote) do
            {:ok, _vote} ->
              User.karma_update(user_id, 1)
              post = Community.get_post!(post_id)
              if post.votes_count == -1 do
                Community.votes_count_update(post_id, 2)
              else
                Community.votes_count_update(post_id, 1)
              end
              post = Community.get_post!(post_id)
              socket =
                assign(socket,
                p_votes_count: post.votes_count,
                active?: "active",
                p_id: post_id,
                u_id: user_id)

              {:noreply, socket}
            {:error, %Ecto.Changeset{} = changeset} ->
              socket = assign(socket, changeset: changeset)
              {:noreply, socket}
          end
        "active" ->
            Votes.delete_vote(Votes.find_vote(user_id, post_id))
            User.karma_update(user_id, -1)
            Community.votes_count_update(post_id, -1)
            post = Community.get_post!(post_id)
            socket =
            assign(socket,
            p_votes_count: post.votes_count,
            active?: "",
            p_id: post_id,
            u_id: user_id)
          {:noreply, socket}
        nil ->
          vote = Votes.find_vote(user_id, post_id)
          case Votes.update_vote(vote, %{downvote: false}) do
            {:ok, _vote} ->
              User.karma_update(user_id, 1)
              post = Community.get_post!(post_id)
              if post.votes_count == -1 do
                Community.votes_count_update(post_id, 2)
              else
                Community.votes_count_update(post_id, 1)
              end
              post = Community.get_post!(post_id)
              socket =
              assign(socket,
              p_votes_count: post.votes_count,
              active?: "active",
              active_dislike?: "",
              p_id: post_id,
              u_id: user_id)

              {:noreply, socket}

            {:error, %Ecto.Changeset{} = changeset} ->
              socket = assign(socket, changeset: changeset)
              {:noreply, socket}
        end
      end
    end
  end

defp current_user_voted?(user_id, post_id) do
    if user_id == false or user_id == "false" do
      ""
    else
      voted? = Votes.find_vote(user_id, post_id)
      if voted? == nil do
        ""
      else
        if voted?.downvote == true do
          nil
        else
          "active"
        end
      end
    end
end
Enter fullscreen mode Exit fullscreen mode
Thread Thread
aminmansuri profile image
hidden_dude • Edited

Yeah that needs refactoring.

Also overuse of if/else and case is not helped with FP popular error handling these days.

A nice thing about exceptions is that you can essentially ignore errors in most places rather than having and if/else everywhere or an error handler everywhere.

That style seems to be devolving into old styles where error handling is mixed in with the happy path making the code harder to read.

Classic OO (Smalltalk) and classic FP (if you consider Common Lisp FP) didn't even really have "if statements" or "case statements".

These were handled in the libraries.

I think modern OO languages biggest flaw is there over reliance on statements and keywords that make them much less extendable than Smalltalk or Lisp where you could literally change everything by extending the libraries.

Really no "modern" OO language really has that flexibility and clean design. Not Java, not C++, not C#, not Ruby, not Python, not JavaScript.

Collapse
rpbaptist profile image
Richard Baptist

The age method does nothing here. You can rewrite it by removing it and renaming the "legal" functions to "can_drink".

Conditional pattern matching with Elixir is great, but in this example I don't see the benefit of this over using "cond" or and if/elsif i other languages, functional or OO.

Collapse
elixirprogrammer profile image
Anthony Gonzalez Author

I guess you mean to remove the can_drink() function and rename legal() to can_drink() like the example below:

defmodule Person do
  def can_drink(age) when is_nil(age), do: "You're not a Person"
  def can_drink(age) when age < 18, do: "Nope!"
  def can_drink(age) when age < 21, do: "Not in the US!"
  def can_drink(age) when age >= 21, do: "YES!!!"
end

IO.puts Person.can_drink(age)
Enter fullscreen mode Exit fullscreen mode

You're right, but I thought that would've make more sense, more readable, and a little bit less confusing for people coming from other languages, the way I did it.

Collapse
begedin profile image
Nikola Begedin

While pattern matching / function clause overloading is great, and one of the many advantages elixir offers, AFAIK, it's not a trait exclusive to functional languages.

Collapse
elixirprogrammer profile image
Anthony Gonzalez Author

That's correct!