Elixir: A primer for object-oriented programmers

Quickstart / reference guide

Roy de Jong
11 min readAug 13, 2018

This article is intended as a (very) basic reference for programmers who are looking to get started with Elixir, but primarily have experience with object oriented languages.

I hope this article is useful if you’re just getting started with Elixir for the first time, or if you just need a quick refresher on the important stuff.

🤓 This is a living document that I will maintain and update as I learn more about Elixir myself. I hope it will become more complete and useful over time.

What is Elixir?

Elixir is a functional programming language based on Erlang/OTP. It is specifically geared towards building large, scalable, fast and fault tolerant applications.

Tech companies like Discord use it successfully to power millions of concurrent, realtime connections. Elixir gives you a relatively new but growing ecosystem, with huge potential.

For more information, refer to the official website linked below. This is also where you’ll find the docs and additional learning resources.

Elixir, compilation and the VM

Elixir code is compiled to platform-independent bytecode. The Erlang Virtual Machine (VM) can then run that bytecode.

This process is comparable to Java, for example — code is compiled to a platform-independent binary that is executed by a VM. In the Java example, the binary is represented as a portable .jar that is then executed by the JRE.

In Elixir, we’re compiling to *.beam files that are executed by the Erlang VM (which is also known as BEAM).

In that sense, Elixir is unlike interpreted (scripting) languages like JavaScript and PHP which do not normally go through a compilation process but are instead interpreted from source code in realtime.

Getting started

First things first: let’s just get some code running.

Installation

To get started, download and install Elixir from its official website. The packages include command line tools, the VM, and everything you need to compile and run Elixir code.

Hello world

Once installed, we’ll have the mix (the Elixir build tool) and IEx (the Elixir Interactive Shell) command line tools available to us. Let’s use the build tool to quickly generate a new application. From a terminal, run:

$ mix new hello_world

This will create a hello_world directory with our basic application outline. Edit the file lib/hello_world.ex to look like the code below:

defmodule HelloWorld do
def hello do
IO.puts "Hello world"
end
end

Let’s start an interactive Elixir session. From the project directory, let’s use iex with the “-S mix” flag to automatically build and load our project’s modules:

$ iex -S mixInteractive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)iex(1)> HelloWorld.hello Hello world:ok

We just compiled our project using the mix build tool, started an interactive session for it with IEx, and called the function from the module we defined.

If you want to quickly try out some code, as we’ll be doing in some examples below, you can also simply run iex without arguments from any directory.

OK — now we can run code, so let’s dive a little deeper.

Functional versus Object Oriented

Values, not references

As a functional programming language, Elixir doesn’t have a concept of classes, objects or instances.

Let’s think about variables. In object-oriented programming, a variable refers to a memory address, which has a set value. We can usually pass that variable around (“by reference”), so that other functions can access and modify it.

Modify a variable’s value in one function, and that changes it in every other context that holds the same reference. Here’s a crude example of this in JS:

var myObj = { val: 100 };function calc(obj) {
obj.val *= 2; // Example: set obj.val to current value times two
}
console.log(myObj.val); // Output: 100
calc(myObj);
console.log(myObj.val); // Output: 200

Such a concept does not exist in Elixir. All data is immutable. When you assign a variable, you actually “bind” a value (that will never change) to a memory address. You can then pass that value around, but without reference.

Bottom line: In Elixir, a world without references, we always pass by value.

Fault tolerance

I want you to get excited about this new world without references. While it changes our approach to writing code a bit, it is very elegant in its own way.

A function in Elixir — one without references — does not depend on state, and cannot be affected by it. It takes input, processes it, and then returns some output. That makes functions extremely predictable and testable.

Overall, this helps with our stability, fault tolerance, and makes debugging a lot easier. Plus, it takes away the pain of threading / concurrency issues as you will never have to worry about “protecting” your state.

Modules and functions

Modules 101: Basics

Instead of classes, we have modules in Elixir. Consider modules a collection of functions. Each is available to your whole application, and its functions can be called from anywhere. It’s all “public”, “static” and stateless.

Modules start with the defmodule keyword, followed by the module name. The module block ends with the end keyword. No curly braces here.

defmodule Some.Module.Name
(...)
end

While namespaces don’t exist in Elixir, it is convention to use the dot character in module names to establish hierarchical structure. IDEs will understand.

Because Elixir compiles your code, modules are thrown onto a big pile and hierarchy doesn’t matter at runtime. The rules are flexible: you can use whatever file extension you want, and you can put multiple modules in one file, for example. Most naming and organisation is convention, not rule.

Side note: Your module names should either be capitalized (SomeModule), or be defined as an atom (:some_module). Otherwise Elixir will try to resolve your module name as a variable (someModule -> undefined variable error).

Functions 101: Basics

We can define functions within modules using the def keyword. Let’s take a look at our Hello World example, which is inside its own module:

defmodule HelloWorld do
def hello do
IO.puts "Hello world"
end

end

We’ve defined a function here called hello, that takes no arguments, and has no return value. If it is placed in the module HelloWorld, it’s full signature becomes HelloWorld.hello/0. The zero indicates an overload with 0 args.

Let’s expand this example and modify our function to take some input:

def hello(subject) do
IO.puts "Hello #{subject}"
end

Nothing too fancy here. We can test this in the interactive shell:

$ iex -S mix
Compiling 1 file (.ex)
Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> HelloWorld.hello("test")
Hello test
:ok
iex(2)> HelloWorld.hello()
** (UndefinedFunctionError) function HelloWorld.hello/0 is undefined or private. Did you mean one of:
* hello/1(hello_world) HelloWorld.hello()

As you can see, we can call our function directly with some string input. Trying to call the function without the parameter results in an error, because Elixir cannot find a function that matches. Note that the function signature is now hello/1 instead of hello/0.

Function returns

The last statement in your function will be used for a return value. There is no explicit return keyword in Elixir, nor is there a “break out” keyword that lets you exit your function prematurely.

So — it’s extra important to make all your functions small, simple units.

Anonymous functions

You can define inline anonymous functions as well using the fn keyword followed by an (optional) parameter list and -> delimiter, and ending with the end keyword.

iex(1)> sayhi = fn -> IO.puts "Hi" end
#Function<20.127694169/0 in :erl_eval.expr/5>
iex(2)> sayhi
#Function<20.127694169/0 in :erl_eval.expr/5>
iex(3)> sayhi()
** (CompileError) iex:3: undefined function sayhi/0
iex(3)> sayhi.()
Hi
:ok

We can invoke anonymous functions by applying a dot (.) between the variable and parentheses.

Anonymous functions are closures. They can be passed around as function parameters, and they have access to variables from the scope in which they are defined (of course only by value).

Basic types

Atoms

An atom is a constant, where the name equals its value. This is synonymous with Symbols in Ruby, for example.

Atoms are denoted by a leading colon (“:”) character and may contain letters, numbers, underscores (_) and @-symbols. They may also end in a question mark or exclamation point.

iex> :foo
:foo
iex> :foo == :bar
false

Several system constants are also atoms. For example, true, false and nil are all atoms. Additionally, when you define something with a capital letter, like module names, it becomes an alias for what actually becomes an atom.

For a deeper dive on atoms, read here.

Strings

A string in Elixir is defined by double quotes. All strings are UTF-8 encoded.

iex> "hellö"
"hellö"

You can concatenate strings using the #{} syntax, as you saw in the Hello World example:

iex(1)> subject = "world"
"world"
iex(2)> "hello #{subject}"
"hello world"

Additional notes on wrangling strings:

  • You can use escape sequences using the backslash, like you’re used to. For example, “\r\n” to insert a line break.
  • Strings are internally represented as binary sequences of bytes.
  • Use byte_size(str) for the byte count, and String.length(str) to get the number of characters from a string.
  • You can use is_string/1 to check whether a value is a string.

Strings versus atoms

Strings are sometimes used interchangeably with atoms, but they are not the same. At a low level, they are represented differently.

Simply put: Atoms are strings that can be tested for equality faster. When you compare strings, you’re actually comparing byte values which simply takes longer. Atoms have less overhead and take up much less memory.

Booleans

Honorable mention: true and false are your booleans. They are atoms. You can use is_boolean/1 to check whether a value is a boolean.

Lists

Arrays don’t exist here— instead we have lists. Lists are denoted by square brackets [], and may contain values of any type.

iex> [1, 2, true, 3]
[1, 2, true, 3]

You can use the length function to get a list’s length (item count):

iex> length [1, 2, 3]
3

Two lists can be concatenated or subtracted by using the ++/2 and — /2 operators respectively:

iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]

As with any other operator or data type, lists are immutable. Every operation therefore returns a new list instead of modifying the old one.

The head of the list is the first item in the last, and the tail is the remainder of the list. You can use the hd and tl functions to get the head and the tail of a list respectively:

iex> list = [1, 2, 3]
iex> hd(list)
1
iex> tl(list)
[2, 3]

Charlists

If a list contains only printable ASCII numbers, Elixir will automatically recognize and treat that list as a charlist. This is literally a list of ASCII codes, a raw representation of text.

This automatic conversion may cause confusion when you see it represented in iex. An innocent looking list with numbers may suddenly act like text:

iex> [11, 12, 13]
'\v\f\r'

You can also use single quotes to define a charlist from text:

iex> 'hełło'
[104, 101, 322, 322, 111]

It’s important to note that these charlists are still lists in every sense, and can be seen and used as such:

iex> is_list 'hełło'
true
iex> 'hello'
'hello'
iex> List.first('hello')
104

In practice, we don’t really use charlists except when interfacing with (older) Erlang libraries. We can use to_string/1 and to_charlist/1 to convert back and forth between strings and charlists:

iex> to_charlist "hełło"
[104, 101, 322, 322, 111]
iex> to_string 'hełło'
"hełło"
iex> to_string :hello
"hello"
iex> to_string 1
"1"

Tuples

Tuples are similar to lists. They can contain any set of values, but are defined with curly braces:

iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2

Getting the length of a tuple, or the value of an individual element is quick and easy:

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
iex> tuple_size(tuple)
2

The major difference between tuples and lists is in optimization. Tuples are optimized for fixed-sizes with quick lookups, but modifications are expensive. Lists are more suitable for variable sizes.

Basic Arithmetic

Working with math and numbers is mostly predictable, and as expected:

iex> 1 + 2
3
iex> 5 * 5
25
iex> 10 / 2
5.0

Division using the / operator always returns a float. To perform integer division or get the division remainder (modulo), use div and rem functions:

iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1

You can also express numbers in binary, octal and hexadecimal and forms:

iex> 0b1010
10
iex> 0o777
511
iex> 0x1F
31

Floats in Elixir are 64-bit double precision.

To define a float, you must include the dot character with at least one decimal. The e for scientific notation is also supported:

iex> 1.0
1.0
iex> 1.0e-10
1.0e-10

Rounding a number can be done with round/1. You can use trunc/1 to ge t the integer part of a number:

iex> round(3.58)
4
iex> trunc(3.58)
3

Concurrency and processes

Process basics

Elixir provides concurrency through processes. Code runs in a process, and each process is independent and separated from other processes. They can communicate by sending messages to each other.

A process, in this case, does not refer to an operating system process. Instead, it refers to a unit of work that the Erlang VM executes across all CPU threads.

Because these processes are managed by the VM, they are extremely lightweight. So lightweight in fact, it’s not uncommon to have (hundreds of) thousands of them running at the same time.

Each process runs (a chain of) functions and stops when it is done.

“Let it crash”

You may see this catchphrase when you’re reading about Elixir. It might sound a bit counter-intuitive, but the key takeaway is that crashing is cheap in Elixir.

Without state, and with each process simply running (a chain of) functions, a crashing process has limited impact to the overall application and can easily be replaced or retried.

Using processes

If we take our sample module, for example:

defmodule HelloWorld do
def hello(subject) do
IO.puts "Hello #{subject}"
end
end

We can execute this function asynchronously by using spawn to run it as a new process. spawn takes a reference to the function and a list of parameters to pass to it, and returns the process ID:

iex> spawn(HelloWorld, :hello, ["world"])
Hello world
#PID<0.120.0>

Process messaging

Running code asynchronously is easy, but processes will often have to communicate back and forth (to send results, for example). We do this via message passing.

We use the send/2 function to send a message, and the receive function to receive data from the other party.

A common design pattern is to use a recursive loop to be able to receive multiple messages in a module:

defmodule HelloWorldPrinter do
def ourListenFunction do
receive do
{:print_hello, subject} -> IO.puts("Hello #{subject}")
end
ourListenFunction()
end
end

This is an example of applying pattern matching. receive do will run our target function if the tuple matches (:print_hello, var)

We can then start this as a process and send some messages to it as an example:

iex> printerPid = spawn(HelloWorldPrinter, :ourListenFunction, [])
#PID<0.110.0>
iex> send(printerPid, {:print_hello, "world"})
Hello world
{:print_hello, "world"}
iex> send(printerPid, {:print_hello, "there"})
Hello there
{:print_hello, "there"}

For more on messaging, process linking, process monitoring, agents and tasks refer to https://elixirschool.com/en/lessons/advanced/concurrency/.

Next steps

Hope you’ve found this useful so far!

Feel free to let me know your thoughts, questions and suggestions in the comments below.

--

--