Some days back, I started learning Elixir. And boy oh boy, learning Elixir is an experience in itself. There were a number of peculiarities I found while going through this functional programming language.
Also, for the record, I am following the book Elixir in Action to learn this beautiful language.
Right off the bat, these were the things I noticed to be different than any language I have ever worked with:
1. Inspired by Ruby and Lisp
You can clearly see Ruby’s influence in the syntax, and Lisp’s influence in the functional ideas. It feels familiar, but also very different at the same time.
2. No return statements
There are no return statements in Elixir. Yes, you heard it right. What it actually does is treat the last expression as the return value.
For example:
defmodule Geometry do
@type sides :: pos_integer()
@type length :: number()
@type area :: float()
@type error_reason ::
:invalid_sides
| :invalid_length
@type result :: {:ok, area()} | {:error, error_reason()}
@spec polygon_area(sides(), length()) :: result()
# Branch 1: invalid number of sides
def polygon_area(sides, _length) when sides < 3 do
{:error, :invalid_sides}
end
# Branch 2: invalid side length
def polygon_area(_sides, length) when length <= 0 do
{:error, :invalid_length}
end
# Branch 3: valid polygon
def polygon_area(sides, length) do
area =
sides * length * length /
(4 * :math.tan(:math.pi() / sides))
{:ok, area}
end
end
Here, you can see how the function returns different values based on certain conditions, without a single return keyword.
3. No for / while loops
Yup. Elixir does not rely on for or while loops the way most languages do. Instead, it heavily uses recursion for iteration.
You might think this would blow up the call stack. But Elixir handles this with something called tail call optimization. When the last thing a function does is call itself, it reuses the stack frame instead of creating a new one.
For example, summing a list:
defmodule ListHelper do
def sum(list) do
do_sum(0, list)
end
defp do_sum(current_sum, []) do
current_sum
end
defp do_sum(current_sum, [head | tail]) do
new_sum = head + current_sum
do_sum(new_sum, tail)
end
end
The defp keyword defines private functions. Or rather, a macro. Which brings me to the next point.
4. A lot of things are macros
Many things that look like keywords are actually macros built on top of Elixir itself. This includes def, defp, and more.
This makes the language very flexible and powerful, but also a bit mind-bending when you are new to it.
5. Data is immutable
You cannot change data in Elixir. You can only create new data.
Even the = operator is not an assignment operator. It is used for pattern matching.
So when you write:
a = 1
a = 2
You are not changing the value of a. You are rebinding it to a new value. The old value still exists somewhere in memory until the garbage collector cleans it up.
6. Pattern matching is everywhere
Pattern matching is not just a feature. It is everywhere.
Function arguments, case statements, variable binding, and more. Once you get used to it, it feels very natural and very powerful.
7. The pipe operator is super handy
The pipe operator lets you chain function calls in a clean way.
-5
|> abs()
|> Integer.to_string()
|> IO.puts() # prints "5"
This keeps code readable and easy to follow.
8. Everything runs in processes
Elixir uses lightweight processes, not OS threads. These processes are cheap, isolated, and communicate only by sending messages.
This makes concurrent programming much simpler and safer compared to shared-memory models.
9. Crashing is okay
Elixir follows the “let it crash” philosophy. Instead of trying to handle every error everywhere, you let processes fail and let supervisors restart them.
This leads to systems that are easier to reason about and more reliable in the long run.
10. Error handling is explicit
Most functions return tuples like {:ok, result} or {:error, reason}.
You are forced to handle both cases, usually with pattern matching:
case File.read("data.txt") do
{:ok, content} -> IO.puts(content)
{:error, reason} -> IO.inspect(reason)
end
No surprises. No hidden behavior.
11. Tooling is great
The mix tool makes project management simple.
Creating a project:
mix new my_app
Running tests:
mix test
Everything just works.
12. It forces you to think differently
Elixir does not let you write sloppy code. Immutability and explicit data handling push you to be clear about what your code is doing.
At first, it feels uncomfortable. Later, it feels safe.
Final thoughts
Elixir is not easy if you are coming from an object-oriented background. It challenges your habits at every step.
But if you stick with it, it rewards you with clean code, strong guarantees, and a fresh way of thinking.
I am still very early in my Elixir journey, but so far, it has been a fun and humbling experience. And it has been kind of growing on me. I am excited to see where this goes.
