Learn elixir with me!

by Adam Davis

Table of contents:

Over the past couple years, I’ve spent most of my development time writing javascript. While javascript is a versatile language, I was itching to learn something new. That’s when elixir caught my attention.

Standing on the shoulders of giants

Elixir is a modern language built on top of the erlang virtual machine (beam). While elixir is a pretty modern language, erlang has been around since 1986.

I first heard about erlang when I took a theory of programming languages course in college. My professor told us about how erlang had made it possible for telecom companies to make extremely scalable and fault-tolerant systems.

This 2015 article from Wired discusses how WhatsApp was able to leverage the power of erlang to support 900 million users with only 50 engineers.

Elixir pairs erlang’s battle-tested feats of engineering with clean syntax and a modern tool set.

Getting functional

Elixir is a functional language, so it works a bit differently than the programming languages I’ve used before.

When I first started writing in elixir, there were three things that stood out to me: returns, scope and piping.

Want to follow along with the examples? You can install elixir by following the instructions here or use an online elixir interpreter here.


Every block in elixir has a return value. But if you skim over some code, you won’t notice any return statements.

That’s because the return value of any block is implied by the value returned by the last statement in that block.

To make a function return the string "Hello, world!", all you need to do is declare the string:

def hello_world() do
  "Hello, world!"

While the above example is trivial, things get more interesting when you start returning values from blocks that may not return values in other languages.

def assign_from_if() do
  a = if 1 > 0 do
  a + 1

In this example, we have an if block that always executes (because 1 is always greater than 0). The if block then returns a value of 10, which is assigned to the variable a. Then, the function returns a + 1, which is equal to 11.

Returning a value from every block may just seem like an interesting quirk, but its value starts to make me sense when we dive into…


While I’m still learning about the intricacies of scope in elixir, one thing becomes apparent after playing around with the language:

A block has access to the values defined in outer scopes, but it cannot affect the bindings in those scopes.

But what does that actually mean? Here’s an example in javascript:

let a = 10;

function increment() {

function print() {

print(); // 10
print(); // 11

Functions in javascript are able to change the values of variables they have access to. The variable a is not defined in the function increment, but the function is able to reassign the variable anyways.

For contrast, here’s an example in elixir:

# Note: the function IO.puts logs a value to the console
def scope_demo() do
  a = 1
  if true do
    IO.puts(a) # 1
    a = a + 1
    IO.puts(a) # 2
  IO.puts(a) # 1

The if block has access to the variable a, which is declared outside of the if block. However, it doesn’t have the ability to re-assign that variable. Instead, on the line a = a + 1, the variable a is shadowed.

But why does the scope work this way? Why can’t we re-assign a variable from within a nested block?

The main answer for this is to limit side effects. Having few to no side effects is a major component of functional programming. In this way, you can have more blocks and functions that are “pure”, meaning that they will produce the same output if given the same input.

When you have a lot of pure functions with minimal side effects, this lends well to…


If you’ve ever used bash, this is a concept you’ll likely be familiar with. Piping is a feature that allows you to use the output of one statement as the input to the next.

For example, to count the number of items in a directory you could pipe the results of ls (which lists the items in a directory) into wc -w (which counts the number of words in the given input).

ls | wc -w

Pipes in elixir work in a similar fashion. The value that is piped into a function is used as the first argument in the function call.

Let’s break down an example:

def ends_with_z?(str) do
  |> String.last()
  |> String.downcase()
  |> Kernel.==("z")

It’s okay if this example doesn’t immediately make sense (I’ve intentionally used some syntax that’s specific to elixir).

Here’s some things you need to know in order to understand this code:

I’m going to publish a more detailed post on elixir pipes in the near future, so subscribe to my newsletter or follow me on DEV so you don’t miss it.

How I’ve been learning

I’ve been learning elixir with a combination of reading and working through exercises. The following resources have been the most helpful for me:

General documentation and help

Practice and deeper learning

Adam Davis

Share this post: