In this article, we are going to explore do's and don't's of writing Elixir code. We will briefly learn the key challenges of switching from other languages to Elixir. For simplicity, we will write all the examples and code snippets using Python & Elixir and learn some common mistakes that programmers often do when switching to Elixir from a different programming language.
What is Elixir?
Elixir is a functional progamming language that runs on the powerful Erlang VM. Elixir takes full advantage of the VM and provides the perfect environment to the developer to build fault tolerant,low-latency, distributed systems.
According to stackoverflow's 2022 Developer Survey, it is the Second most loved Programming language of the Year.
The Elixir Community is growing as more and more people & companies adopting elixir to their code base.
The Key Challenges of Switching to Elixir
Unlike other functional programming languages such as Erlang, Clojure, etc. Elixir has a more beginner friendly syntax. This also opens the door to bring the baggage of other programming languages to Elixir.
For example, Python programmers with no previous functional programming experience might attempt to apply coding patterns of Object Oriented programming that are not suitable for Functional Programming. Even though, they might get away with writing codes that are more "Pythonic" than "Elixir". It will hinder their growth & productivity in the long term.
The lack of functional programming knowledge will also prevent them from taking advantage of various functional programming paradigms. It will also challenge their core concepts in at least three different layers:
- The Functional way of thinking (i.e. immutability/streams/transformation/pipeline)
- OTP way of thinking (i.e. concurrency, fault-tolerence, distributed system)
- Patterns and Pipes.
So they will have to unlearn what they already know and think with a fresh perspective. This initial step is relatively hard but once, they master the concepts they will become productive in no time.
Now let's take a look at a few examples:
Mutable vs Immutable
In a Python environment, Variables or objects are mutable. Consider the following code snippet:
my_list = [1, 2, 3, 4]
do_something(my_list)
print(my_list)
Here, we can't reliably say, what will be the output of the print statement without looking at the source code of function do_something
. In Python, Lists are passed as reference, so it is possible that the do_something
function can alter the original my_list
e.g. add something to the list or remove a thing or two. If we think in concurrency or parallel programming this could lead to unintended bugs. Two processes accessing the same object can get different values based on execution order or moment.
In Elixir, this is not an issue cause, variables are always immutable:
my_list = [1, 2, 3]
do_something(my_list)
IO.inspect(my_list)
Here the output of IO.inspect()
is guaranteed to get the list [1, 2, 3]
. In other words, the do_something()
function can not modify the my_list variable. Even if we spawn hundreds or thousands of processes and access my_list
all of those processes will get the same value of my_list
. This is also true for complex data such as Nested List of Lists, Maps, or Database Records, etc.
Mastering the concepts of immutability is essential to write Good Elixir Code.
Nested Function Calls vs Piping
Nested function calls are common in most programming languages. When we want to pass the return value of a function as an agrument to another function the nesting occurs. it is more like a series of transformation to data. Elixir also allows nested function calls to transform data. Consider the below example:
decorate(bake(add_flavor(mix(ingredients))))
You will have to take some time to read the statement carefully and figure out which function is getting called in which sequence, what are their effects. And, how the data is getting transformed in each steps.
Fortunately, Elixir solves this problem elegantly by introducing a new operator called "Pipe" (|>
). It takes the result of the expression to its left/top and inserts it as the first parameter of the function invocation to its right/bottom. Using the pipe operator we can rewrite the above example:
ingredients
|> mix()
|> add_flavor()
|> bake()
|> decorate()
Here the steps & sequence of function calls are clear. We are passing the ingredients
to mix()
. The result of mix()
is getting piped to add_flavor()
. Then we bake()
the result and finally we decorate()
The Bottomline is, do not write Elixir codes with nested function calls use piping instead. It will make your code clear, concise and much more readable.
Functions & Pattern Matching
Pattern Matching is one of the most powerful feature of Elixir Programming Language. Mastering pattern matching techniques will boost productivty and improve code quality. The key is to know when and where we can apply pattern matching. Let's explore the below python function:
def flip(coin):
if coin == "head":
return "tail"
elif coin == "tail":
return "head"
else:
return "invalid"
This function simply flips a coin. If the coin is head
it returns tail
and vice versa. It also returns invalid
for any other arguments. Now, if we want we can write it in a similar way in elixir:
def flip(coin) do
if coin == "head" do
"tail"
else
if coin == "tail" do
"head"
else
"invalid"
end
end
end
This sure works, but it doesn't look good. First of all, it has a nested if...else
block. Whenever you notice your code has nested blocks, take a few moment and think it thoroughtly. There must be a better way of doing it. In the above case, we do have a better way of achieving the same functionality. We can refactor this code with 3 simple lines of code:
def flip("head"), do: "tail"
def flip("tail"), do: "head"
def filp(_), do: "invalid"
Looks neat, isn't it? We are using pattern matching to match the argument directly from the function parameter. When the function flip
will be called, Elixir will take each method signature and match it with the input argument. As soon as the match occurs, it will execute the function and stop. So for example, if we call the flip
function with the parameter "tail". Elixir will first try to match the argument with the first function once, it fail it will move to the next one. Since, the second function takes "tail" as argument, it will get executed. We will get an output "head".
It will also work for complex Data Structure such as Dictionary or Maps. For example, see the below function that takes a dictionary person
as argument and prints the full name.
def get_fullname(person):
firstname = person.get("firstname")
lastname = person.get("lastname")
return f"{firstname} {lastname}"
The dict person
has two key value pairs, firstname
and lastname
. We first retrieve the values from the dict and store them in two variables and later we print them by concatenating them using f-strings.
In Elixir, we can do the pattern matching in the function signature like below:
def get_fullname(%{firstname: first_name, lastname: last_name}) do
"#{first_name} #{last_name}"
end
just like dict
Elixir has a data structure named Map which we are using for this example. The firstname
and lastname
are atoms, they are the key of the Map. The first_name
and last_name
variable will get the value of the Map using pattern matching. And, in the function body we just return the concatenated string.
The above examples are pretty basic but, they give us the glimpse of how pattern matching works. And, how it can help us to improve Elixir code.
Conclusion
Before calling it a day, let's quickly recap what we've explored so far. We learned the key challenges of switching from different programming language to Elixir. We also explored some common mistakes and how to avoid them by taking advantage of Elixir features and best practices.
Top comments (0)