DEV Community

Cover image for 5 curious facts about Elixir
Willian Frantz for X-Team

Posted on

5 curious facts about Elixir

Elixir is a functional programming language that runs on the BEAM virtual machine, itself used to implement Erlang. Elixir extends Erlang and provides interoperability between both languages. Its syntax is easy to understand and its tools help to standardize code so it's easier to move from one project to the next.

My name is Willian Frantz and I've been an Elixir Alchemist since 2017. I actively contribute to the Brazilian programming community by writing blog posts, speaking at events, and contributing to open-source libraries.

Despite being used by companies such as Pinterest, Slack, and Discord, Elixir remains a niche programming language. That's why I want to give you 5 curious facts about Elixir. Hopefully, they will encourage you to give the Elixir programming language a look.

1. Functional, but Dynamic too

Elixir is a functional language, but it also allows you to rebind a variable. This is only possible because of the pattern matching operator =. For example:

iex> a = "Something"
iex> a = "Else"
iex> IO.puts(a)
"Else"
Enter fullscreen mode Exit fullscreen mode

2. There's No Assign Operator

That's right. Elixir has no assign operator. Instead, it uses = as its main pattern matching function. Every time you use =, you're pattern matching. For example:

iex> a = ["Something", "Else"]
iex> IO.puts(a)
"SomethingElse"

iex> ["Something", a] = ["Something", "Else"]
iex> IO.puts(a)
"Else"

iex> %{name: name} = %{name: "Willian"}
iex> IO.puts(name)
"Willian"
Enter fullscreen mode Exit fullscreen mode

Pattern matching is an amazing functionality in Elixir. You can use it to write complex business logic and decision-making capabilities into your code. For example, let's use it to create function polymorphism:

def hello("") do
  raise "Empty string provided"
end

def hello(name) do
  IO.puts("Hello World, #{name}!")
end
Enter fullscreen mode Exit fullscreen mode

In this example, if we call the hello/1 function (with /1 indicating the number of arguments it expects) and give it an empty string as its main argument, it will raise an exception with the message "Empty string provided". Otherwise, it will print "Hello World, {name}".

Another way to create function polymorphism is by using bodyguards. Elixir has some default functions called guards that you can use to control structures (like case/do) or apply to a function scope. For example:

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

def hello(var) do
  raise "Expected a String, received: #{var}"
end
Enter fullscreen mode Exit fullscreen mode

We added a guard called is_binary/1 to our function, which checks if the name is actually a string. If it fails to validate the string, it will raise an exception that says "Expected a String, received: {var}".

3. Elixir Goes Meta

Here's a mind-blowing fact: 92% of Elixir's source code is written in Elixir. That's why dealing with constructs is so flexible in Elixir. The programming language is meta: it allows you to write code that writes code, so you can, for example, extend your Elixir program with macros.

Let's use an Elixir pipe operator to solve chainable calls:

# without pipe operator
Enum.frequencies(String.split("Repeated Chars on String", "", trim: true))

# result:
%{
  " " => 3,
  "C" => 1,
  "R" => 1,
  "S" => 1,
  "a" => 2,
  "d" => 1,
  "e" => 3,
  "g" => 1,
  "h" => 1,
  "i" => 1,
  "n" => 2,
  "o" => 1,
  "p" => 1,
  "r" => 2,
  "s" => 1,
  "t" => 2
}

# Same code with pipe operator
"Repeated Chars on String"
|> String.split("", trim: true)
|> Enum.frequencies()
Enter fullscreen mode Exit fullscreen mode

The pipe operator breaks chainable calls into a sequential, readable list of code. You could read this as:

I have a string "Repeated Chars on String"
I want to split that string by character and trim it
I want the frequencies of the generated list
We can actually pipe into control structures:

# without pipe operator
if Enum.sum(1..10) == 55 do
  IO.puts("1..10 sum = 55")
end

# with pipe operator
1..10
|> Enum.sum()
|> Kernel.==(55)
|> if do
  IO.puts("1..10 sum = 55")
end
Enter fullscreen mode Exit fullscreen mode

While I wouldn't recommend to use a pipe operator in this case, it's a good demonstration of how flexible Elixir can be.

4. How Elixir Deals with Strings

Imagine we have both a string "Hey I am a String" and a charlist 'Hey I am a Charlist'. We can cast either whenever we need:

iex> "to Charlist" |> to_charlist()
'to Charlist'

iex> 'to String' |> to_string()
"to String"
Enter fullscreen mode Exit fullscreen mode

We can also interpolate or concatenate strings:

# Interpolation
iex> a = "some"
iex> b = "thing"
iex> "String: #{a}#{b}"
"String: something"

# Concatenation
iex> "String: " <> "something"
"String: something"
Enter fullscreen mode Exit fullscreen mode

Since Elixir deals with strings as sequences of bytes, we can also use binary operations to create strings:

iex> <<"String: ", "some", "thing">>
"String: something"
Enter fullscreen mode Exit fullscreen mode

Or combine binaries with pattern matching:

iex> <<"String: ", rest::binary>> = "String: something"
iex> rest
"something"
Enter fullscreen mode Exit fullscreen mode

5. There Are No Loops

Elixir doesn't have loops. Instead, it uses list comprehensions, recursion, and higher-order functions to iterate over a collection of items. Let's go over each in order.

Here's an example of how you can use list comprehensions to generate a list of squared numbers:

iex> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]
Enter fullscreen mode Exit fullscreen mode

Elixir handles recursion pretty well too. Because recursive calls are optimized, they use almost the same amount of space as an imperative loop. Take this programmed factorial, for example:

defmodule Fact do
  def call(1), do: 1
  def call(a), do: call(a - 1) * a
end

iex> Fact.call(5)
120
Enter fullscreen mode Exit fullscreen mode

We can recursively calculate the factorial of a number without having any call-stack issues. This works even with a much bigger number:

iex> Fact.call(50)
30414093201713378043612608166064768844377641568960512000000000000
Enter fullscreen mode Exit fullscreen mode

Last but not least, we have higher-order functions, a set of functions that take another function as input (e.g. map, reduce, filter, etc). Let's say we want to find all the even numbers in a list.

iex> Enum.filter(1..20, &Integer.is_even/1)
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Enter fullscreen mode Exit fullscreen mode

In this example, we used:

  • Enum.filter/2 to filter our collection.
  • 1..20 to generate a list of numbers from 1 to 20.
  • A capture operator & into Integer.is_even/1 as the function that the filter uses.

In Conclusion

I hope I've captured your interest with these 5 curious facts about Elixir. In my opinion, Elixir has a few unique features that make it stand out from other languages. If you'd like to know more, check out the following Elixir resources:

Discussion (0)