🇧🇷 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 thenGreetUser.hello()
. In the former we would callhello/1
and the last we would callhello/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" if
s 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) bellowusers_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 functionfn user_name -> ... end
that will be executed for each element on the list - Inside the anonymous function we called the
capitalize/2
from ElixirString
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:
- Official website: https://elixir-lang.org/
- Elixir School: https://elixirschool.com/pt/
- Tiny documentary of the language: https://www.youtube.com/watch?v=lxYFOM3UJzo
- Elixir lang on twitter: https://twitter.com/elixirlang
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 🖖
Top comments (0)