Skip to content

Latest commit

 

History

History
668 lines (492 loc) · 23.2 KB

File metadata and controls

668 lines (492 loc) · 23.2 KB

🇧🇷 Artigo original em português: Entendendo Elixir com a ajuda de PHP

After some years and differents experiences with PHP and some frameworks like WordPress, Laravel, Symfony and Phalcon, I had the opportunity to work with Elixir and since then it has been my go to language for new projects.

For those who have never had a previous experience with a functional language this first contact could be a little confusing, that was my case when I first start with Elixir.

The "help" offered in this article by PHP it's just a merely comparison made between the two languages, which by the way are NOTHING similar.

In this article I've just implemented the same functionalities using both languages so you can compare the logic of a new programming paradigm with a more familiar one offered by PHP.

I will approach the Elixir functionalities that I most use on a daily basis applying the 80/20 rule, but I'm sure that with this simple knowledge you can read a .ex or .exs file and don't lose yourself on the way.

Before we begin...

If you don't know Elixir at all I suggest that you read at least the Starting Guide on their docs. It will show the syntax, the primitives types and operators of the language.

Without further ado, let's dive in!

Pattern Matching

The best pattern matching definition is the following:

Pattern matching is a powerful part of Elixir. It allows us to match simple values, data structures, and even functions - Elixir School

But what exactly does this mean? To better understand this we need to take a look at the match operator =. This operator can attribute values to variables and can be used to decompose more complex structures, like tuples and lists.

# iex

iex(1)> {a, b, c} = {"São Paulo", "Rio de Janeiro", "Fortaleza"}
{"São Paulo", "Rio de Janeiro", "Fortaleza"}

iex(2)> a
"São Paulo"

iex(3)> b
"Rio de Janeiro"

iex(4)> c
"Fortaleza"

# Another example
iex(5)> {_, "Ecuador", country} = {"Brasil", "Ecuador", "Chile"}
{"Brasil", "Ecuador", "Chile"}

iex(6)> country
"Chile"

Also, you can fetch the first item of a list

# iex

iex(1)> [head | tail] = ["Olá", "Hello", "Hola"]
["Olá", "Hello", "Hola"]

iex(2)> head
"Olá"

iex(3)> tail
["Hello", "Hola"]

Case a tuple can't be match against the left hand side of the operator, that means, maybe a tuple don't have the same number of fields or the same expected value of the left side of the = a exception will occur:

# iex

iex(1)> {a, b} = {"São Paulo", "Rio de Janeiro", "Fortaleza"}
# ** (MatchError) no match of right hand side value: {"São Paulo", "Rio de Janeiro", "Fortaleza"}
#    (stdlib) erl_eval.erl:453: :erl_eval.expr/5
#    ...

iex(1)> {_, "United States", country} = {"Brasil", "Ecuador", "Chile"}
# ** (MatchError) no match of right hand side value: {"Brasil", "Ecuador", "Chile"}
#    (stdlib) erl_eval.erl:453: :erl_eval.expr/5
#    ...

The above code gave us an understanding of the first half of the pattern matching definition. What about the other half that allow us to look for patterns in functions definitions? Let's take a look:

To help with this part I'll create a PHP class so you can compare with the equivalent Elixir module. To start with let's just create a class and module that have a hello/1 function that receive the user_name as parameter.

PHP class

<?php
// ./GreetUser.php

class GreetUser
{
    public function hello($userName)
    {
        echo "Hello, " . $userName;
    }
}

$greetUser = new GreetUser;
$greetUser->hello("Joe");

Elixir module

# ./greet_user.exs

defmodule GreetUser do

    def hello(user_name) do
        IO.puts("Hello, #{user_name}")
    end

end

GreetUser.hello("Joe")

So far, so good, right? We have a hello/1 function that will print "Hello, Joe" both in PHP class and in Elixir module.

But let's assume that the user_name parameter could be nil (null in PHP). How can we adjust our code to attend this requirement?

PHP class

<?php
// ./GreetUser.php

class GreetUser
{
    public function hello($userName = null)
    {
        if(is_null($userName)) {
            echo "Hello world!";
            return;
        }
        echo "Hello, " . $userName;
    }
}

$greetUser = new GreetUser;
$greetUser->hello("Joe");
$greetUser->hello(null);

Using PHP we could add a default value to the $userName and handle this with a if statement inside our function in case $userName is really null and display a different message.

If execute GreetUser.php we would get:

"Hello, Joe"
"Hello world!"

Although the same solution is possible in Elixir, we can use pattern matching and remove the complexity of the code from our function by get rid of the if statement:

# ./greet_user.exs

defmodule GreetUser do

    def hello(nil) do
        IO.puts("Hello world!")
    end
    def hello(user_name) do
        IO.puts("Hello, #{user_name}")
    end

end

GreetUser.hello("Joe")
GreetUser.hello(nil)

As there are two functions with the same name, the chosen one will be the one that has the correct parameter value.

This means that only when user_name = nil the first function will be executed, in any other case it will skip this function definition and go to the next hello/1 declared within this module.

It's important to say that due to function arity GreetUser.hello(nil) is different then GreetUser.hello(). In the former we would call hello/1 and the last we would call hello/0 which does not exists and would break. Always have the arity in mind!

Any primitive of the language can be used in pattern matching. So let's assume that the GreetUser module now have to greet a specific message to users that have the name "Jane", we could achieve this by doing this:

PHP Class

<?php
// ./GreetUser.php

class GreetUser
{
    public function hello($userName)
    {
        if($userName === "Jane") {
            echo "Nice to see you, Jane!";
            return;
        }
        echo "Hello, " . $userName;
    }
}

$greetUser = new GreetUser;
$greetUser->hello("Joe");
$greetUser->hello("Jane");

Elixir Module

# ./greet_user.exs

defmodule GreetUser do

    def hello("Jane" = user_name) do
        IO.puts("Nice to see you, #{user_name}!")
    end
    def hello(user_name) do
        IO.puts("Hello, #{user_name}")
    end

end

GreetUser.hello("Joe")
GreetUser.hello("Jane")

This time we specified the user_name value and add the match operator to the parameter: ("Jane" = user_name). By doing this we are matching user_name to "Jane" and them we can use the variable inside our function. If you don't want to use the parameter inside your function you can dismiss it by prepending it with an underscore: ("Jane" = _user_name). Elixir understand that this variable will never be used.

Declared variables that are not used will raise a warning in compilation time, keep that in mind!

I hope that with this example you began to see how pattern matching can remove complexity away from our code. Doesn't Elixir module look more simple and better to read than the class in PHP? We no longer have to "calculate" ifs statement to know what a function will return, pretty good, right?

Guards

If you enjoy the wonders of pattern matching you can go beyond using guards. It will allow you to make more complex parameter validation in your functions declaration.

Guards are defined using a when after a function declaration. While pattern matching is useful to check explicit values, with guards we can apply some boolean logic to it. We can check a parameter's type, length, if it is nil, if it is higher or lower given a determined number and some others validations.

Back to our GreetUser, we will use guard to validate if user_name is a string.

Elixir module

# ./greet_user.exs

defmodule GreetUser do

    def hello(user_name) when is_binary(user_name) do
        IO.puts("Hello, #{user_name}")
    end

end

GreetUser.hello("Joe")

The is_binary/1 function will return a boolean value and according to it (true or false) hello/1 get invoked or not. Elixir has several type-checks” functions like this so you can check a expression type.

Here is a tip. Either using a pattern matching or guard it's always a good practice to at least think about a alternative in case of a parameter mismatch. You could get a runtime error if a parameter won't match a function

** (FunctionClauseError) no function clause matching in GreetUser.hello/1

The above error will happen if we call hello/1 with an integer: GreetUser.hello(1).

As an alternative we could add another declaration of hello/1 just dismissing the parameter, so everything that is not a string will be direct to it:

# ./greet_user.exs

defmodule GreetUser do
   
    def hello(user_name) when is_binary(user_name) do
        IO.puts("Hello, #{user_name}")
    end
    def hello(_) do
        IO.puts("Only string is allowed in this function")
    end

end

GreetUser.hello("Joe")
GreetUser.hello(1)
Important!

The function declaration order is extremely important!! In the above example we restrict hello/1 to only accept string. No problem there, but what if we had reversed the function declaration orders by placing hello(_) before the hello(user_name) when is_binary(user_name)?

# ./greet_user.exs

defmodule GreetUser do
   
    def hello(_) do
        IO.puts("Only string is allowed in this function")
    end
    def hello(user_name) when is_binary(user_name) do
        IO.puts("Hello, #{user_name}")
    end

end

GreetUser.hello("Joe")
GreetUser.hello(1)

Running the code we would get the following: warning: this clause cannot match because a previous clause at line 5 always matches

Our code still run with no errors, but hello/1 with the guard when is_binary/1 would never be called because hello/1 without the guard will ALWAYS be evaluated first! So pay attention to function's declaration order!!

Pipe Operator

This one is a bit trick, but in a nutshell the pipe operator is represented by the symbol |> and all that it does is pass an expression evaluation (or result) as the first parameter to the next one.

With this you can create a function pipeline (get the name?) given a expression (variable or function).

To illustrate this concept, now GreetUser has a new requirement, we'll need to greet some users which their names will came in a list. To do that we'll create another function named hello_group/1. This new function will receive the list of names as users_name.

# ./greet_user.exs

defmodule GreetUser do
    def hello_group(users_name) when is_list(users_name) do
        # code
    end
    def hello_group(_) do
        IO.puts("Only lists are allowed")
    end
end

We declared the function and added a guard so we can validate that the parameter is indeed a list, and defined a "default" hello_group/1 in the chance that users_name is not a list.

The next step it's to handle the users_name using the pipe operator. First we'll go through the list and normalize the names. The first part can be achieve using the map/2 function from Elixir's Enum module.

# ./greet_user.exs

defmodule GreetUser do
    def hello_group(users_name) when is_list(users_name) do
        users_name
        |> Enum.map(fn user_name -> String.capitalize(user_name) end)
    end
    def hello_group(_) do
        IO.puts("Only lists are allowed")
    end
end

Let's breakdown this code:

  • We called users_name inside our function
  • Then we added the |> (pipe operator) bellow users_name. It could be called by the right side as well.
  • To loop the list, we use the map/2, that will return another list.
  • As map/2 second parameter we gave a anonymous function fn user_name -> ... end that will be executed for each element on the list
  • Inside the anonymous function we called the capitalize/2 from Elixir String module to capitalize the names.

The bit that you need understand is that we pass only map/2 second parameter because the first parameter is passed by the |>.

Remember: "The pipe operator forwards the result of an expression as the first parameter on to the next expression". In our case the first expression is the variable users_name and when it gets evaluated will return the list of names. And our pipeline worked because map/2 expected as the first parameter a list!

Using PHP we could achieve the same with this code:

<?php
// ./GreetUser.php

class GreetUser
{
    public function helloGroup($usersName)
    {
        if(!is_array($usersName)) {
            echo "Only arrays are allowed";
            return;
        }

        $usersNameNormalized = array_map(function ($name) {
            return ucfirst($name);
        }, $usersName);
    }
}

The next step is to "glue" all the names in a single string. We'll use join/2 which is another function from Enum module. This function will receive in the first parameter a list and in the second parameter receives a string as the joiner element. We'll use just a ", " to separate the names.

Elixir module

# ./greet_user.exs

defmodule GreetUser do
    def hello_group(users_name) when is_list(users_name) do
        users_name
        |> Enum.map(fn user_name -> String.capitalize(user_name) end)
        |> Enum.join(", ")
    end
    def hello_group(_) do
        IO.puts("Only lists are allowed")
    end
end

PHP class

<?php
// ./GreetUser.php

class GreetUser
{
    public function helloGroup($usersName)
    {
        if(!is_array($usersName)) {
            echo "Only arrays are allowed";
            return;
        }

        $usersNameNormalized = array_map(function ($name) {
            return ucfirst($name);
        }, $usersName);

        $usersNameString = implode(", ", $usersNameNormalized);
    }
}

Our pipeline still functions because map/2 will return a new list and the |> forwards it as the first parameter to the join/2 function.

Great! hello_group/1 is able to normalize and glue together the names. Now we just need to print a message to the user.

So far the result of hello_group/1 would be a string with the users names. We could use hello/1, which receives a string as first parameter to print the message to our user.

# ./greet_user.exs

defmodule GreetUser do

    def hello(user_name) when is_binary(user_name) do
        IO.puts("Hello, #{user_name}")
    end
    def hello(_) do
        IO.puts("Only strings are allowed")
    end

    def hello_group(users_name) when is_list(users_name) do
        users_name
        |> Enum.map(fn user_name -> String.capitalize(user_name) end)
        |> Enum.join(", ")
        |> hello
    end
    def hello_group(_) do
        IO.puts("Only lists are allowed")
    end
end

GreetUser.hello_group(["joe", "jane", "jim"])

By adding hello/1 in the pipeline we fulfill all the requirements for hello_group/1.

Take a look at the same feature in PHP

<?php
// ./GreetUser.php

class GreetUser
{
    public function hello($userName)
    {
        echo "Hello, " . $userName;
    }

    public function helloGroup($usersName)
    {
        if(!is_array($usersName)) {
            echo "Only arrays are allowed";
            return;
        }

        $usersNameNormalized = array_map(
            function ($name) { return ucfirst($name); },
            $usersName
        );

        $usersNameString = implode(", ", $usersNameNormalized);

        return $this->hello($usersNameString);
    }
}

$greetUser = new GreetUser;
$greetUser->helloGroup(["joe", "jane", "jim"]);

We could make the above code better by splitting in different functions, but do notice that even if we make such change the code in Elixir has a LOT more readability and less complexity.

Here is another way to visualize the same code without using the pipe operator:

# ./greet_user.exs

defmodule GreetUser do

    def hello(user_name)do
        IO.puts("Hello, #{user_name}")
    end

    def hello_group(users_name) do
        users_name
        |> Enum.map(fn user_name -> String.capitalize(user_name) end)
        |> Enum.join(", ")
        |> hello
    end

    def hello_group(users_name, :notpiped) do
        hello(
            Enum.join(
                Enum.map(
                    users_name,
                    fn user_name -> String.capitalize(user_name) end),
                ", "
            )
        )
    end
end

GreetUser.hello_group(["joe", "jane", "jim"])
GreetUser.hello_group(["joe", "jane", "jim"], :notpiped)

Here we defined another function: hello_group/2, despite the arity, both functions will return the same result. But hello_group/1 is cleaner and easier to understand.

Control Structures

In Elixir we have access to certain tools that allow us to remove complexity from our code, but sometimes we have no other option and we have to use a if or a switch. The control structures available to us are: if, unless, case, cond and with. This article is an overview of the language and because of that I'm not write about all this structures. I'll focus in the with because this one can be harder to understand than the others.

The first thing that we need to understand is that in a functional language the result of a function is always the result of the last expression that get executed, we don't have the return keyword in this functional world. Because of this, control structures will return the result of the last expression, so you need to be careful when write them. This can be challenging at first, but you get used to it quite fast.

In a functional language we cannot do some like this:

public function anotherFunction() {
    $param = null;

    if ($condition) {
        $param = "PHP";
    }

    return $param;
}

In the above PHP code we are defining a default value to $param and based on $condition's his value will change. This wouldn't work in a Elixir code because de if is an expression! If this was an Elixir code, all that this if expression was doing is to match $param to "PHP", and the return of this expression would be an :ok atom. The $param variable would never match "PHP" outside the if statement.

To this work in Elixir, one could do as the following:

def another_function do
    param = if condition do
        "Elixir"
    else
        nil
    end
end

By doing this, the return of the if would be matched to the param variable. With that in mind, let's move on.

With

You can think in with like a pipeline that checks the result of each expression and if some of those return an unexpected value the code inside the with block won't get executed. You can also add fallback clausules to handle some unexpected behaviour or errors.

Let's add this structure in our hello/1 function and see how it works:

# ./greet_user.exs

defmodule GreetUser do

    def dummy_function, do: :ok

    def hello(user_name)do
        with :ok <- dummy_function() do
            IO.puts("Hello, #{user_name}")
        end
    end

end

GreetUser.hello("Joe")

We created an dummy_function/0 that will only return an :ok atom and we added an with block to check the returned value. If the returned value was indeed an :ok atom, them we print "Hello, Joe" on the screen.

If dummy_function/0 won't return an :ok we could add a fallback clausule to handle this:

# ./greet_user.exs

defmodule GreetUser do

    def dummy_function, do: :error

    def hello(user_name)do
        with :ok <- dummy_function() do
            IO.puts("Hello, #{user_name}")
        else
            _ ->
                IO.puts("dummy_functtion didn't return an :ok atom")
        end
    end

end

GreetUser.hello("Joe")

We can match variables and other structures on the left side of the <- operator, so we can use the value returned by the function inside de with block:

# ./greet_user.exs

defmodule GreetUser do

    def dummy_function, do: {:ok, "dummy function"}

    def hello(user_name)do
        with {:ok, _response} <- dummy_function() do
            IO.puts("Hello, #{user_name}")
        else
            {:error, message} ->
                IO.puts("dummy_functtion return an error: #{message}")
            _ ->
                IO.puts("I can't figure what dummy_function has returned")
        end
    end

end

GreetUser.hello("Joe")

We checked if dummy_function/0 will return an {:ok, response} result, if it does the function proceed and print the greeting. If dummy_function/0 returns an {:error, message} it prints the message with the error. And at last if dummy_function/0 returns something unexpected the last clausule, which matches everything, will be executed printing the generic error message. The last clausule will not match on the other cases ({:ok, response} and {:error, message}) because the others two clausules will match first, remember, the declaration other matter!

With this knowledge we can create an pipeline of functions that will be validated in each step and handle any possible error individually:

def hello(user_name)do
    with {:ok, response1} <- dummy_function(),
         {:ok, _response2} <- dummy_function_2(reponse1) do
            IO.puts("Hello, #{user_name}")
    else
        {:error, message} ->
            IO.puts("dummy_functtion return an error: #{message}")
        _ ->
            IO.puts("I can't figure what dummy_function has returned")
    end
end

And there you have it!

If you can wrap your mind around these Elixir's concepts I'm guarantee that you will have the basic knowledge to handle quite some understanding of an Elixir's code.

Here are some topics for you to get deeper into the language and its ecosystem

Mix

Task runner (similar to composer on PHP), link to the guide and docs

Phoenix

Web framework (similar to Symfony, Laravel, etc... on PHP), link to the site and docs

Ecto

ORM (similar to Doctrine on php), link to the guide and docs

Hex

Package repository (similar to Packgist of Composer), link to the site

References

And here, some usefull links and references that I used to write this article:

I hope that you enjoyed read this article and that was useful to you. If you have any suggestion or feedback leave a comment. This is one of my first articles and I would like to know how I'm doing 😁

Thank you for your time! See you soon 🖖