DEV Community

Cover image for How functional programming made me a better developer, with Elixir
Nicolas Lima
Nicolas Lima

Posted on

How functional programming made me a better developer, with Elixir

On this article, I'll share my experience with functional programming, and how it helped me to became a better developer, practicing good programming principles, like SOLID, KISS, YAGNI. Also share some Elixir examples, and applicability on Object Oriented Programming, like JavaScript.

Functional Programming paradigm is a model that use only functions to solve your problems, avoiding mutations, states changes, and all things that can provoke mutate in a data or process, handling your data entire all process through only functions. If you thought like me, you probably got scare thinking about how to implement an algorithm that you don't have an object, class instance, all the OO (Object Oriented) particularities that we already know, right? That's make me a bit confused, then i had to think outside my box, and try to model and design my problems solutions using a different way. And that was the process that make me start to became an better developer.

In my situation, I gotta to learn Elixir, that is a functional language with its syntax inspired by ruby (I guess 🤔). Elixir has a syntax not so friendly at beginning (at least for me lol), but is auto-explainable, and that is where the process start, in Elixir (Functional languages in general, but I'll focus on Elixir), you are forced to write codes self-explainable, cause in the most part, your code will be a lot of functions calling themselves at side of a logical very well defined, otherwise, you will suffer with "over engineering" and confused implementations, and just with that, you already is getting inside on a software engineering principle very important, that are "KISS" - Keep It Simple Stupid. To this usage for an example, we gonna use a pipe operator (Reference), its syntax is |> what it does, is to pass the previous value as a first argument of the function next to it. Lets imagine the follow scenario, a quite basic, but.. We have a model "Product", and we must create a service, to check if this product has on stock.

# Pseudo-code (Abstracted logical and implementations)

def product_is_available(product_id) do
  product_id
  |> get_product()
  |> product_has_stock?()
end
Enter fullscreen mode Exit fullscreen mode

Note this example have an defined flow, you know at each line clearly whats is going on, that is, you getting the product_id, getting product through id, with this product found, check for product availability (Has stock), when is necessary, you can apply in your code a flow like that, independent of language, making your code to have a good structure, let's apply the same example in JavaScript:

// Pseudo-code (Abstracted logical and implementations)

function productIsAvailable(productId) {
  const product = getProduct(productId);
  return productHasStock(product);
}
Enter fullscreen mode Exit fullscreen mode

the code is a little shorter, but the clearly flow is the same.

Pattern Matching

In Elixir, you have a nice feature, that is pattern matching. Basically you have an input, and an expected value, so lets imagine the expected_value is "cake" and your input is "cupcake". If you compare booths, there's no match, cause string "cupcake" doesn't match with "cake". So, lets imagine we have a map, that contains a program language, it would be defined as %{language: "Elixir"}, so lets create a function is_elixir? that checks if a given map, is for language elixir:

def is_elixir?(%{language: "Elixir"}), do: true
def is_elixir?(language_map), do: false
Enter fullscreen mode Exit fullscreen mode

Whats happening here? When we pass our map language = %{language: "Elixir"}, and call this function is_elixir?(language) it tries to proceed on the first match, that is our first function definition, but, if we have a map like %{language: "C++}", and try to call the same function, there's no match on the first function, then, its search for the next match, that is reached on our second function (Because the signature generalizes language variable value, not requiring to be an specific value). So, what if we call is_elixir? without any arguments? It will raise an exception ´"No pattern match with function is_elixir? /0"` (/0, /1, /n.. it means the number of arguments of a function), whats happened was: it tried to match with first function clause, but no success, then tried to the second one, but no success again, then left no third to test, so it raise this exception because of that.

We can demonstrate a kind of pattern matching in JavaScript with destructing, lets do the same example:

`

function isElixir({ language }) {
  return language === "Elixir";
}
Enter fullscreen mode Exit fullscreen mode

in this case, we receiving an object, and destructing it through function signature, the difference is, when object given (or not object), doesn't has a key "language", it will raise an exception "Cannot read property language", it can be handled with a try catch clause.

Getting more deep...

Elixir doesn't have classes, or properties. It have modules, with their functions, to work with. So, when you thought in OOP, you remember, that if a class have a lot of responsability and differents contexts togheter, it will bring a lot of readability problems, and violates the first principle of SOLID, single responsability. Bringing it to Elixir, it became even worst, because all you have is a lot of mixed functions, with even more mixed contexts/domains, obviously have codes and projects written like that, but is a terrible practice. So, segregating all theses contexts in anothers modules, you will practice single responsability principle, and now you have an code with modules properly segregated, and you became able to manipulate it, maintain it, integrate with services, and what else you need.

Now, lets mix these concepts, and get through examples even more deeps. In Elixir we have an feature called "with", that is one of my favorite features. "With" works like you having a lot of pipe operators |> but at each pipe, you have a condition (an pattern match for example), and when not satisfy the defined flow, it falls out of the clause, going to an else (when exists), if there no match even on else, a "with clause match" exception will be raised.

So lets imagine a kind of the product domain that we had discussed aboove, but lets imagine we have a scenario that interact with anothers contexts, all these through its services.

This scenario was a kind off an real problem that i dealed with in one of my personal projects, that are, we have an authenticated user, and we suposed to get its current geolocation, to store it, and send it to a client who consum it, a little complex, right? lol.

PS: In a real scenario, the best way to avoid this is to write an middleware to prevent non logged users to access/use a feature. These examples are only for lessons purposes.

So lets to the code

# Pseudo-code (Abstracted logical and implementations)

def treat_current_location(user, location) do
  with {:ok, true} <- User.is_authenticated?(user),
       {:ok, coords} <- Location.get_coordinates(location),
       {:ok, _} <- Location.save_last_coords(coords) do
    response(%{
      message: "location successfully stored",
      last_location: coords,
    }, 200)
  else
       {:unauthorized, _} -> response("current user is not logged", 401),
       {:error, _} -> response("an unexpected error ocurred", 500),
  end
end
Enter fullscreen mode Exit fullscreen mode

Note in that code, we used pattern matching, on each with clause, when not satisfies, it tries to match in else clauses. Note with this with this scenario looked at like a recipe of cake, there are defined instructions, and the only thing you need to do, is follow this defined flow. Now lets apply this example for JavaScript, that was the real project solution.

// Pseudo-code (Abstracted logical and implementations)

function treatCurrentLocation(user, location) {
  try {
    if (User.isAuthenticated(user) === false) return response("non authenticated", 401);
    const { coords } = Location.getCoordinates(location);
    Location.saveLastCoords(coords);

    return response({
      message: "location successfully stored",
      lastLocation: coords,
    }, 200);

  } catch (error) {
    console.log(error);
    return response("an unexpected error ocurred", 500);
  }
}
Enter fullscreen mode Exit fullscreen mode

In that code, any raised error will be catched by our try catch, so if destructs got no success, or the Location module not return the expected value, all be catched properly. And also, you have a defined flow of each thing is hapenning.

In this example, you were able to practice YAGNI (You ain't gonna need it), discarding all useless process or values, just continued following an defined flow ruled by KISS (Keep it simple..).

So that was a little bit of good practices that i know applied on Elixir with a little comparison of JavaScript, my favorite language, there's still many content to share about, Elixir, JavaScript and best practices.. I hoped you had enjoy the content ;)

Top comments (0)