Elixir
Initially I thought I could write a 10 page Elixir introduction with this chapter. Just to provide the basics to get you going for using the Phoenix and the Ash Framework. All that by avoiding any chicken-or-egg situations - where an explanation relies on concepts yet to be discussed.
I failed! I ended up with a chapter about Elixir which covers a lot. It is meant to be read in a linear fashion. But feel free to just skip parts and come back later (or don’t).
If this is your first functional programming language you might want to get a cup of coffee first. It might take a while to get used to the functional programming paradigm. It took me a long time too!
Elixir Version Our journey is charted for Elixir version 1.15. Please use the following command to check the installed Elixir version:
|
If you’re new to Elixir or in need of an upgrade, you might want to consider using asdf , a versatile version manager that can handle both Elixir and Erlang (you need both). Head over to the asdf homepage for instructions on how to install and use it. |
Elixir’s Interactive Shell (IEx)
Elixir equips you with an interactive shell, IEx
(Interactive Elixir), that’s going to be our sandbox for a lot of examples in this chapter. So let’s roll up our sleeves and give it a whirl in your command line:
$ iex
Erlang/OTP 26 [erts-14.0] [source] [64-bit] [...]
Interactive Elixir (1.15.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> (1)
1 | This is your IEx prompt. |
To bid goodbye to IEx, give CTRL-C a double tap or a single CTRL-\ will do. |
IEx provides autocomplete (just tap TAB when you’re unsure) and a command history (the arrow-up key is your friend). |
Do you like video tutorials? Have a look at the "IEx Basics in 55 Seconds" video in our @elixir-phoenix-ash YouTube Channel. |
Hello world!
The classic! We use iex
to run the function IO.puts/1
which prints a string to standard output:
iex> IO.puts("Hello world!")
Hello world!
:ok
Always enclose strings with double quotes! Single quotes create a charlist, which is a different type. In case you need double quotes within a string you have to escape them with backslashes:
iex> IO.puts("With double quotes: \"Hello world!\"")
With double quotes: "Hello world!"
:ok
Let’s have a quick look at IO.puts/1
:
IO
is the name of the IO module. A module is collection of functions. It is a way to organize code. Normally a module has to be loaded withrequire
orimport
but since theIO
module is so essential it gets loaded automatically.puts/1
is the name of a function. The final1
ofIO.puts/1
is called a Function Arity. The arity represents the number of arguments that function accepts. A module can contain multiple functions with the same name as long as they all have a different arity.
We discuss modules and functions more detailed in modules and functions.
Debugging Essentials
In this introductory guide, we’re only scratching the surface of debugging in Elixir, but I’d like to introduce three vital tools that will be beneficial while exploring the code examples in this book.
dbg/2
Elixir version 1.14 introduced the powerful debugging tool dbg/2
. It not only prints the passed value, returning it simultaneously, but also outlines the code and location. Here’s an example:
iex(1)> name = "Elixir"
"Elixir"
iex(2)> dbg(name)
[iex:2: (file)]
name #=> "Elixir"
"Elixir"
iex(3)> dbg(IO.puts("Hello World!"))
Hello World!
[iex:3: (file)]
IO.puts("Hello World!") #=> :ok
:ok
Beginners often find it odd that dbg/2 and IO.inspect/2 return the value they print. However, once you start utilizing Pipes (which we’ll discuss in The Pipe Operator (|>)), it becomes a natural part of maintaining uninterrupted code flow while inspecting values in a pipe operation. |
Have you recognized that dbg/2 has an arity of 2 but we only use one argument? That is possible because the second argument is optional (hence the options \\ [] part). |
IO.inspect/2
The function IO.inspect(item, opts \\ [])
is a staple in Elixir debugging. Although it’s less feature-rich than dbg/2
, its usage remains widespread, given its history and straightforward application. You can inject IO.inspect/2
into your code at any point, printing the value of an expression to your console - perfect for verifying a variable’s value or a function call’s result.
For example:
iex> name = "Elixir"
"Elixir"
iex> IO.inspect(name)
"Elixir"
"Elixir"
Feel free to always use dbg/2 instead of IO.inspect/2 . However, if you’re working with older codebases, you’ll likely encounter IO.inspect/2 . |
i/1
Finally, the IEx helper function i/1
offers useful insights about any data type or structure. Launch an IEx session with iex
in your terminal, and then call i()
with any term to obtain information about it.
Here’s an example:
iex> name = "Elixir"
"Elixir"
iex> i(name)
Term
"Elixir"
Data type
BitString
Byte size
6
Description
This is a string: a UTF-8 encoded binary. It's printed surrounded by
"double quotes" because all UTF-8 encoded code points in it are printable.
Raw representation
<<69, 108, 105, 120, 105, 114>>
Reference modules
String, :binary
Implemented protocols
Collectable, IEx.Info, Inspect, List.Chars, String.Chars
This output elucidates that "Hello, world!"
is a 13-byte BitString and provides further details like the string’s raw representation and the protocols it implements.
Modules and Functions
In Elixir, we organize code in a structured way using modules and functions.
For Elixir beginners we now run into a typical chicken and egg problem. Because how can I show you how Modules and Functions work while you have never used any Elixir code? I try to use code examples which should be easy to understand. |
Modules are like containers for functions, which represent reusable pieces of code.
Let’s illustrate this with an example:
iex> defmodule Greetings do (1)
...> def hello(name) do (2)
...> "Hello, " <> name <> "!" (3)
...> end
...> end
{:module, Greetings,
<<...>>, {:hello, 1}} (4)
iex> Greetings.hello("Alice") (5)
"Hello, Alice!"
1 | We use the defmodule keyword to define a module. The module’s name always begins with a capital letter. |
2 | We define a function within the module using the def keyword. |
3 | The function concatenates the input name with a greeting message. |
4 | The return value represents the module creation. |
5 | We call the function from outside the module using this syntax. |
Both defmodule and def use a do … end structure to denote the start and end of the block. |
Module names use CamelCase and start with a capital letter, while function names are in snake_case . |
As an exercise, let’s save the following module to the file greetings.exs
:
defmodule Greetings do
def hello(name) do
"Hello, " <> name <> "!"
end
end
To use the hello/1
function in the Greetings
module, we need to load and compile greetings.exs
with Code.require_file("greetings.exs")
in iex
:
$ iex
Erlang/OTP 26 [erts-14.0] [source] [64-bit] [...]
Interactive Elixir (1.15.0) - press Ctrl+C to exit (...)
iex(1)> Code.require_file("greetings.exs")
[
{Greetings,
<<70, 79, ...>>}
]
iex(2)> Greetings.hello("Bob")
"Hello, Bob!"
Function Arity
In Elixir, the term "arity" refers to the number of arguments that a function accepts. Functions are identified by both their name and their arity, expressed as name/arity
.
The arity concept is essential because it allows us to define multiple functions with the same name but different arities within a single module. Each of these is considered a distinct function due to its unique argument count.
Let’s demonstrate this with a slightly more complex example:
iex> defmodule Greeting do
...> def greet() do (1)
...> "Hello, world!"
...> end
...>
...> def greet(name) do (2)
...> "Hello, #{name}!"
...> end
...>
...> def greet(name, time_of_day) do (3)
...> "Good #{time_of_day}, #{name}!"
...> end
...> end
iex> Greeting.greet()
"Hello, world!"
iex> Greeting.greet("Alice")
"Hello, Alice!"
iex> Greeting.greet("Bob", "morning")
"Good morning, Bob!"
In this example, we’ve defined three versions of the greet
function, each with a different arity.
1 | The greet/0 function, which takes no arguments, returns a generic greeting. |
2 | The greet/1 function accepts one argument and provides a personalized greeting. |
3 | The greet/2 function takes two arguments and offers a personalized greeting that also includes the time of day. |
Private Functions
Sometimes, we want to hide certain functions within a module, making them inaccessible from outside. Elixir supports this through private functions, which we declare using the defp
keyword:
iex> defmodule SecretMessage do
...> def public_message(name) do
...> secret() <> name
...> end
...>
...> defp secret do (1)
...> "Secret Hello, "
...> end
...> end
iex> SecretMessage.public_message("Alice") (2)
"Secret Hello, Alice"
iex> SecretMessage.secret (3)
** (UndefinedFunctionError) function SecretMessage.secret/0 is undefined or private
SecretMessage.secret()
1 | secret/0 is a private function and can only be accessed within its module. |
2 | public_message/1 is public, and can be called from outside its module. It can access secret/0 because they’re in the same module. |
3 | Attempting to call secret/0 from outside its module results in an UndefinedFunctionError because it’s a private function. |
Private functions help us hide implementation details and reduce the exposed interface of a module, leading to cleaner and more maintainable code.
Hierarchical Modules
As your projects become more complex, it’s crucial to structure your code into a clear and manageable form. In Elixir, you can achieve this by using hierarchical module names. Hierarchical modules are defined by attaching sub-module names to a parent module using a .
delimiter.
Here’s a new example with a Fruit Shop:
iex> defmodule FruitShop.Apples do
...> def price_per_kg() do
...> 10
...> end
...> end
iex> FruitShop.Apples.price_per_kg()
10
The .
syntax offers a neat shortcut for defining nested modules. Here’s how you can create the same hierarchy using nested module definitions:
iex> defmodule FruitShop do
...> defmodule Apples do
...> def price_per_kg() do
...> 10
...> end
...> end
...> end
iex> FruitShop.Apples.price_per_kg()
10
Both methods achieve the same result. Your choice between these two depends on your project’s structure and your coding style preference.
Import
The import
directive in Elixir provides a way to access public functions from other modules without needing to write out their fully qualified names. This can make your code cleaner and easier to read.
Consider the following FruitShop.Apples
module:
iex> defmodule FruitShop.Apples do
...> def price_per_kg() do
...> 10
...> end
...> end
By importing this module, you can call its functions directly, without having to prefix them with the module’s name:
iex> import FruitShop.Apples
FruitShop.Apples
iex> price_per_kg()
10
Here, importing FruitShop.Apples
lets you call price_per_kg/0
directly, eliminating the need to use the FruitShop.Apples.
prefix.
Selective Importing
While importing a module grants you access to all its public functions, there might be times when you want to import only specific functions from a module. Elixir allows you to do this using a selective import.
For instance, suppose the FruitShop.Apples
module also had a quantity_in_stock/0
function. But if you only needed price_per_kg/0
in your current context, you could import just that function like so:
iex> defmodule FruitShop.Apples do
...> def price_per_kg() do
...> 10
...> end
...> def quantity_in_stock() do
...> 100
...> end
...> end
iex> import FruitShop.Apples, only: [price_per_kg: 0]
FruitShop.Apples
iex> price_per_kg()
10
Here, import FruitShop.Apples, only: [price_per_kg: 0]
means that only the price_per_kg/0
function from FruitShop.Apples
is available for direct calling. This can help reduce naming conflicts and makes it clear which functions are being used from the imported module.
An alternative to only
is except
, which lets you import all functions except the ones specified.
Alias
The alias
directive offers a convenient way to assign a shorter, alternative name to a module. This can improve both readability and maintainability of your code by reducing verbosity when accessing the module’s functions.
Take a look at the FruitShop.Apples
module:
iex> defmodule FruitShop.Apples do
...> def price_per_kg() do
...> 10
...> end
...> end
To make calling this module’s functions less verbose, you can use the alias
directive to assign it a shorter name:
iex> alias FruitShop.Apples, as: Apples
FruitShop.Apples
iex> Apples.price_per_kg()
10
In the code above, we’ve created an alias for FruitShop.Apples
as Apples
.
For a quicker and more direct way, you can simply use alias FruitShop.Apples
. Elixir will automatically infer the alias from the last part of the module name, in this case Apples
:
iex> alias FruitShop.Apples
FruitShop.Apples
iex> Apples.price_per_kg()
10
In this example, the alias FruitShop.Apples
directive lets you call functions from FruitShop.Apples
using the shortened name Apples
. This can significantly improve readability when working with modules that have long or complex names.
Use
Elixir’s use
keyword is a cornerstone of metaprogramming in Elixir. It is a powerful tool that helps keep our code DRY (Don’t Repeat Yourself) by allowing us to perform certain actions defined in another module within the current module.
I won’t delve too deeply into metaprogramming here. I’m simply covering it to ensure that you can recognize it when you come across it unexpectedly. |
Metaprogramming is a technique that helps us write code that generates or modifies other code. In the context of Elixir, we can think of the use
keyword as a way to inject code from one module into another. This is accomplished through the use of the using
macro in the module that is being used.
Let’s illustrate this with a more comprehensive example involving a Discount
module and two fruit modules, Apples
and Bananas
:
defmodule Discount do
defmacro __using__(_) do
quote do
def apply_discount(price, percentage) do
price - (price * (percentage / 100))
end
end
end
end
defmodule FruitShop.Apples do
use Discount
def price_per_kg() do
10
end
end
defmodule FruitShop.Bananas do
use Discount
def price_per_kg() do
5
end
end
In these examples, both the FruitShop.Apples
and FruitShop.Bananas
modules use the Discount
module. The use
keyword triggers the using
macro in the Discount
module, which in turn injects the apply_discount/2
function definition into the FruitShop.Apples
and FruitShop.Bananas
modules. Therefore, we can call apply_discount/2
directly on either of these modules:
iex> FruitShop.Apples.apply_discount(10, 20)
8
iex> FruitShop.Bananas.apply_discount(5, 15)
4.25
In these cases, we’ve applied a 20% discount to the price of apples (which was 10), and the result is 8. Similarly, we’ve applied a 15% discount to the price of bananas (which was 5), and the result is 4.25.
By leveraging the power of the use
keyword and metaprogramming, we’ve written a versatile Discount
module that can be used across multiple fruit modules to apply discounts to their prices.
If you’re working with a Phoenix application, you might see use ExUnit.Case in your test files. This is a practical example where ExUnit.Case provides a set of functionalities (like assert functions) that will be accessible within your test cases. |
Higher-Order Functions
In Elixir, functions are treated as first-class citizens. This means you can use functions as arguments to other functions, and even return them as results. A function that can take another function as an argument or return it as a result is called a higher-order function.
When passing a function to a higher-order function, we often use anonymous functions. Let’s dive in and understand what these are.
Anonymous Functions
An anonymous function, as the name suggests, is a function without a name. These are throwaway functions that you define right where you need them.
Anonymous functions are defined using the fn
keyword, like so:
iex> hello = fn -> "Hello, world!" end (1)
#Function<20.99386804/0 in :erl_eval.expr/5>
iex> hello.() (2)
"Hello, world!"
1 | We’re defining an anonymous function that returns "Hello, world!" and assigning it to the variable hello . |
2 | The . (dot) operator is used to call anonymous functions. |
Anonymous functions can also take parameters:
iex> add = fn (a, b) -> a + b end (1)
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> add.(1, 2) (2)
3
1 | We define an anonymous function that accepts two parameters and returns their sum. |
2 | This anonymous function can be invoked with two numbers to get their sum. |
Using anonymous functions in Elixir is often easier with the Capture Operator & which is a shorthand version. |
Variables
In Elixir, variable names adhere to the snake_case format. They start with a lowercase letter, and words are separated by underscores (_).
Take a look at the following examples:
iex> length = 10 (1)
10
iex> width = 23
23
iex> room_area = length * width
230
1 | Here, the = operator assigns the value 10 to the variable length . |
Variable names starting with an uppercase letter will throw an error:
iex> RoomWidth = 2
** (MatchError) no match of right hand side value: 2 (1)
1 | The MatchError might seem strange at this point, but fear not! Its mystery will unravel as we dive deeper into Elixir’s realm and learn about pattern matching. |
Variable Scopes
Variable scope is a fundamental concept in Elixir, referring to the area of code where a variable can be accessed or is valid. To better understand how variable scopes work in Elixir, let’s consider the following example using fruits.
defmodule FruitShop do
def fruit_count do
apples = 10 (1)
IO.puts("Apples in the shop: #{apples}")
basket_fruits() (2)
IO.puts("Apples in the shop: #{apples}") (4)
end
defp basket_fruits do
apples = 5 (3)
IO.puts("Apples in the basket: #{apples}")
end
end
1 | Here, we declare the number of apples in the shop as 10. |
2 | We then call the basket_fruits/0 function. |
3 | Inside the basket_fruits/0 function, we declare the count of apples in the basket as 5. |
4 | After coming back from the basket, we check the count of apples in the shop again. |
The output of calling FruitShop.fruit_count()
would be:
Apples in the shop: 10
Apples in the basket: 5
Apples in the shop: 10
As you can see, the number of apples
in the basket_fruits/0
function did not affect the number of apples
in the fruit_count/0
function. This is because, although they have the same name (apples
), they are in different scopes and, hence, are treated as completely different variables.
Immutability
Probably you have already heard about immutability in Elixir. What’s that about?
A variable points to a specific part of the memory (RAM) where the data is stored. In many programming languages that data can be changed to update a variable. In Elixir, you can’t change it. But that doesn’t mean that you can’t rebind a variable to a different value just that this new value gets a new piece of memory and doesn’t overwrite the old memory. They both coexist. Once a function returns a result and therefore, has finished its work, everything gets garbage collected (wiped blank).
Why is that important at all? With immutable variables, we can be sure that other processes can not change their values while running parallel tasks. That has a massive effect. In the end, it means that your Phoenix application can run on multiple CPUs on the same server in parallel. It even means that your Phoenix application can share multiple CPUs on several nodes of a server cluster in your data center; this makes Elixir extremely scalable and safe.
But doesn’t that make your application slower? Funny thing: No. This way is faster. Since it is not efficient to change data in memory.
But don’t worry. It is not as complicated as it sounds. Everytime you use a variable it uses the value of that moment in time. It will not be effected/changed afterwords:
iex> product = "Orange"
"Orange"
iex> test1 = fn -> IO.puts(product) end (1)
#Function<21.126501267/0 in :erl_eval.expr/5>
iex> product = "Apple"
"Apple"
iex> test2 = fn -> IO.puts(product) end
#Function<21.126501267/0 in :erl_eval.expr/5>
iex> product = "Pineapple"
"Pineapple"
iex> test3 = fn -> IO.puts(product) end
#Function<21.126501267/0 in :erl_eval.expr/5>
iex> product = "Banana"
"Banana"
iex> test1.() (2)
Orange
:ok
iex> test2.()
Apple
:ok
iex> test3.()
Pineapple
:ok
iex> IO.puts(product)
Banana
:ok
1 | Those anonymous functions can run on totally different CPUs. Each one lives in its own little universe. |
2 | The value of product has changed multiple times. But for test1.() it is the value from that point in time when we created the function. |
Do you have to understand immutability to learn Elixir? No! Don’t stress yourself. It is a concept that you will learn over time. Just keep it in mind that it is there. |
Control Structures
Control structures are a fundamental aspect of any programming language, and Elixir is no exception. They direct the flow of execution and help us build more complex and powerful programs.
You will not use all of these control structures regularly. Have a look and decide which ones feel useful to you. |
if
, unless
, and else
if
, unless
, and else
are coding classics to create conditional branches which are used in most programming languages. These expressions evaluate to either true
or false
and execute the associated code blocks.
Two styles of syntax can be used for these expressions: multi-line and single-line.
Multi-line Syntax
For complex conditions or actions, or when including an else
clause, the multi-line syntax is most appropriate. It uses do…end
to wrap the executed code block.
# `if` expression
iex> num = 5
iex> if num > 3 do
...> IO.puts("#{num} is greater than 3")
...> end
# `unless` expression
iex> num = 2
iex> unless num > 3 do
...> IO.puts("#{num} is not greater than 3")
...> end
# `if` with `else` expression
iex> num = 2
iex> if num > 3 do
...> IO.puts("#{num} is greater than 3")
...> else
...> IO.puts("#{num} is not greater than 3")
...> end
Single-line Syntax
In a single-line syntax, the do:
keyword follows the condition. This syntax is often used for simple conditions and actions.
# `if` expression
iex> num = 5
iex> if num > 3, do: IO.puts("#{num} is greater than 3")
# `unless` expression
iex> num = 2
iex> unless num > 3, do: IO.puts("#{num} is not greater than 3")
Both styles are equally valid; the choice depends on the specific use case and code readability.
Remember, if , unless , and else are expressions, not statements. They always return a value, which can be assigned to a variable or used in a larger expression. |
if/2
is a Macro
In Elixir, if/2
is a macro, a special kind of function that is executed at compile-time, before your program runs. It gets "translated" into a case/2
expression internally. This doesn’t have to bother you. I just felt that it was good to know. In case you are more interested in this detail have a look at
Understanding if/2
Arity
In Elixir, when we say a function or macro has an arity of 2, we mean it accepts two arguments. The if
macro in Elixir has an arity of 2 because it requires two arguments to work correctly: a condition and a keyword list.
You are now familiar with the if
construct looking something like this:
if condition do
# Code executed when the condition is true
else
# Code executed when the condition is false
end
This is the most common way to use if
in Elixir, and it’s very readable. However, under the hood, Elixir is interpreting this in a slightly different way. This 'do/else' style is actually syntactic sugar, a way to make the code look nicer and easier to understand.
In reality, Elixir sees the if
construct as follows:
if(condition, do: # Code to execute if true, else: # Code to execute if false)
Here, it’s clear to see that if
is receiving two arguments:
The condition to evaluate, which should be either
true
orfalse
.A keyword list that specifies what to
do:
if the condition is true and what to doelse:
if the condition is false.
So when we say if/2
, we’re saying the if
macro with two arguments: a condition and a keyword list.
case
The case
construct in Elixir provides a powerful way to pattern match complex data structures and execute code based on the matching pattern. For many Elixir porgrammers it is the go-to construct for control flow.
A case
expression evaluates an expression, and compares the result to each pattern specified in the clauses. When a match is found, the associated block of code is executed.
iex> num = 2
iex> case num do
...> 1 ->
...> IO.puts("One")
...> 2 ->
...> IO.puts("Two")
...> _ ->
...> IO.puts("Other")
...> end
Two
In the above example, num
is evaluated and its value is compared with each pattern. The pattern 2
matches the value of num
, so "Two"
is printed.
A catch-all clause (_ → ) is often used as the last clause to handle any values not explicitly covered by previous patterns. |
Pattern matching in case
is not limited to simple values. You can also pattern match on more complex structures like tuples, lists, or maps.
iex> tuple = {:ok, "Success"}
iex> case tuple do
...> {:ok, msg} ->
...> IO.puts("Operation successful: #{msg}")
...> {:error, reason} ->
...> IO.puts("Operation failed: #{reason}")
...> _ ->
...> IO.puts("Unknown response")
...> end
Operation successful: Success
In this example, the case
statement matches on the structure and content of the tuple.
Remember, like if
and unless
, case
is an expression, meaning it returns a value which can be assigned to a variable or used in another expression.
iex> num = 3
iex> result = case num do
...> 1 ->
...> "One"
...> 2 ->
...> "Two"
...> _ ->
...> "Other"
...> end
iex> IO.puts(result)
Other
In this example, we use a case
expression to evaluate num
and assign the corresponding string to the result
variable. The variable result
is then printed using IO.puts/1
. The case
expression returns "Other"
, because num
does not match 1
or 2
, and "Other"
is assigned to result
.
Importance of Pattern Order
A critical aspect to understand when using case
is the order of pattern matches. Elixir evaluates the patterns from top to bottom and executes the first pattern that matches, ignoring any subsequent patterns even if they are more precise.
Let’s illustrate this with an example:
iex> tuple = {:ok, "Success"}
iex> case tuple do
...> {:ok, _} ->
...> IO.puts("Operation was OK")
...> {:ok, msg} ->
...> IO.puts("Operation successful: #{msg}")
...> _ ->
...> IO.puts("Unknown response")
...> end
Operation was OK
In this example, even though the second pattern {:ok, msg}
is a better match for tuple
(as it also matches and binds the message), the first pattern {:ok, _}
matches first and so its associated code is executed.
Therefore, when using case , it’s important to order your patterns from the most specific to the least specific to ensure the intended pattern is matched first. |
cond
cond
is another control structure in Elixir that checks for the truthiness of multiple conditions. It is like a collection of multiple if/2
expressions. It evaluates each condition in turn, from top to bottom, and once it encounters a condition that evaluates to true
, it executes the associated block of code and ignores the rest.
Here’s an example:
iex> num = 15
iex> cond do
...> num < 10 ->
...> IO.puts("#{num} is less than 10")
...> num < 20 ->
...> IO.puts("#{num} is less than 20 but greater than or equal to 10")
...> true ->
...> IO.puts("#{num} is greater than or equal to 20")
...> end
15 is less than 20 but greater than or equal to 10
In this example, cond
checks each condition in order. When it finds a truthy condition (num < 20
), it executes the associated block of code and skips the rest.
The true → clause serves as a catch-all clause, similar to the _ → in a case expression. If none of the previous conditions are truthy, the code associated with the true → clause will be executed. |
cond
is especially useful when you have multiple conditions and don’t want to write nested if
statements.
Remember, cond is an expression and it returns a value, which can be assigned to a variable or used in another expression. |
with
and for
with
Keyword
The with
keyword in Elixir is used for a sequence of pattern matches, returning the value of the last expression if all previous matches succeed.
Here’s an example:
iex> fruits = [{:apple, 5}, {:orange, 3}, {:banana, 4}]
iex> with {:apple, count} <- List.keyfind(fruits, :apple, 0) do
...> IO.puts("Found #{count} apples!")
...> else
...> _ -> IO.puts("No apples found.")
...> end
Found 5 apples!
In this example, we have a list of tuples representing different kinds of fruits and their respective counts. We use the with
keyword to pattern match on an {:apple, count}
tuple.
If the pattern match is successful, we print a message saying we found a certain number of apples. If the pattern match fails, we fall back to the else
clause and print a message saying no apples were found.
for
Comprehensions
for
comprehensions in Elixir provide a way to iterate over Enumerables and generate a new list, optionally filtering the elements. The result of this operation can be assigned to a variable.
Here’s an example:
iex> squared_numbers = for num <- [1, 2, 3, 4, 5],
...> do: num * num
[1, 4, 9, 16, 25]
iex> squared_numbers
[1, 4, 9, 16, 25]
In this example, for
iterates over each number in the list [1, 2, 3, 4, 5]
, squares each number (num * num
), and collects the results into a new list. This list is then assigned to the variable squared_numbers
.
You can also filter elements in a for
comprehension using a guard clause:
iex> odd_squares = for num <- [1, 2, 3, 4, 5],
...> rem(num, 2) == 1,
...> do: num * num
[1, 9, 25]
iex> odd_squares
[1, 9, 25]
In this example, only odd numbers are squared. The rem(num, 2) == 1
condition filters out even numbers, so they are not included in the result. This resulting list is assigned to the variable odd_squares
.
Variable Assignment with Control Structures
One of the unique characteristics of control structures in Elixir is their ability to return a value, which can be assigned to a variable. This is possible because these expressions always return a value.
Here’s an example of assigning the result of an if expression to a variable:
iex> num = 5
iex> comparison_result = if num > 3 do
...> "#{num} is greater than 3"
...> else
...> "#{num} is not greater than 3"
...> end
iex> IO.puts(comparison_result)
5 is greater than 3
This approach can be extended to other control structures such as unless, case, and even custom defined ones:
iex> num = 2
iex> comparison_result = case num do
...> 1 -> "one"
...> 2 -> "two"
...> _ -> "other"
...> end
iex> IO.puts(comparison_result)
two
The last expression executed within the block will be the returned value of the control structure. If the condition does not pass and there is no else clause, the expression returns nil`
.
Data Types
Since this is a beginners book I have to discuss all these data types because you’ll use them eventually. But you don’t need to understand them all right away. Feel free to skip the parts which don’t interest you. You can always come back later.
Integer
Integers are whole numbers that can be positive, negative or zero. In Elixir, you can use integers without any limits, as long as you have enough memory in your machine.[1]
Here is an example of integers in Elixir:
iex> 3
3
iex> -1042
-1042
iex> 0
0
Integers can also be represented in binary, octal, and hexadecimal:
iex> 0b1010
10
iex> 0o777
511
iex> 0x1F
31
Additionally, Elixir supports basic arithmetic operations with integers:
iex> 2 + 3
5
iex> 10 - 7
3
iex> 5 * 4
20
iex> 16 / 2
8.0 (1)
1 | The division operation (/ ) in Elixir always returns a float. |
Readability in Large Integers
When working with large integers in Elixir, it’s common to encounter readability issues. A long, continuous string of digits can be difficult to read at a glance, especially when it comes to distinguishing thousands, millions, billions and so on.
To improve the readability of large integers, Elixir allows the use of underscores (_
) as visual separators. You can place these underscores anywhere in your number, and they will be ignored by the compiler.
Here’s an example:
iex> 1_000_000
1000000
In the example above, 1_000_000
is exactly the same as 1000000
in Elixir. The underscores simply make it easier to identify the number as one million at a glance.
This feature is particularly useful when working with very large numbers, or when defining constants that might be better understood in their 'grouped' form.
Remember that while you can place the underscores anywhere in the number, placing them in positions that reflect common numeric groupings (e.g., thousands, millions) tends to be the most effective for readability.
iex> 123_456_789 # easy to read
123456789
iex> 1234_5678_9 # harder to read
123456789
Float
Floats are numbers that have a decimal point. They are represented in Elixir using 64-bit double-precision.
Here’s how you can represent float numbers in Elixir:
iex> 3.14
3.14
iex> -0.1234
-0.1234
iex> 0.0
0.0
Floats in Elixir must be at least 1 digit long, and they can have an optional exponent part. Here’s an example:
iex> 1.0e-10
1.0e-10
Keep in mind that float number precision can be a little imprecise due to the way they are stored in memory.
Remember that in Elixir, as in other languages, when you perform arithmetic with both integers and floats, the result will be a float to maintain precision.
iex> 2 * 3.5
7.0
The use of floor division (div
) and modulo operation (rem
) can return integer values:
iex> div(10, 3)
3
iex> rem(10, 3)
1
String
Overview
In Elixir, strings are binary sequences, encoded in UTF-8. This encoding enables strings to handle any Unicode character, which is a significant advantage when developing international applications.
Elixir strings are defined using double quotes ("
). Here’s an example:
iex> "Hello, World!"
"Hello, World!"
Concatenation
In Elixir, you can concatenate strings using the <>
operator:
iex> "Hello, " <> "World!"
"Hello, World!"
String Interpolation
Elixir provides a powerful feature called string interpolation. It allows you to embed expressions, not limited to just strings but also other data types, directly into a string. The embedded expressions are evaluated and their results are inserted into the original string at the corresponding positions. String interpolation is accomplished using the #{}
syntax.
Apart from strings, Elixir’s string interpolation also works with other data types like integers, floats, atoms, and even lists or tuples of integers or characters. When these data types are interpolated, they are automatically converted to string format within the larger string.
Below are examples that demonstrate string interpolation with various data types:
iex> first_name = "Stefan"
iex> greeting = "Hello #{first_name}!" (1)
"Hello Stefan!"
iex> counter = 23
iex> "Count: #{counter}"
"Count: 23"
1 | Here, we’ve used the #{} syntax to insert the value of the first_name variable into the string. |
This string interpolation feature provides a convenient way to incorporate dynamic content into strings, enhancing the flexibility of text manipulation in Elixir.
Multiline Strings
Elixir also supports multiline strings. You can define a multiline string by wrapping the text in triple double quotes ("""
):
iex> """
...> Hello,
...> World!
...> """
"Hello,\nWorld!\n"
Notice that Elixir automatically inserts newline characters (\n
) where the line breaks occur.
Escape Characters
In certain situations, we might want to include special characters in our strings that can’t be typed directly. For instance, we might want to include newline to split a string across multiple lines.
These special characters can be represented using escape sequences, which are initiated by a backslash (\
). Here are some common escape sequences:
\"
- Double quote\'
- Single quote\\
- Backslash\n
- Newline\t
- Tab
Here are some examples of using escape sequences:
iex> "Hello, \"World!\"" (1)
"Hello, \"World!\""
iex> "Line 1\nLine 2" (2)
"Line 1\nLine 2"
iex> "Column 1\tColumn 2" (3)
"Column 1\tColumn 2"
1 | The \" escape sequence allows us to include double quotes within a string. |
2 | The \n escape sequence represents a newline, which splits a string across multiple lines. |
3 | The \t escape sequence represents a tab, which creates some horizontal space in the string. |
String Functions
Elixir provides a String
module that offers a comprehensive set of functions for working with strings. With these functions, you can perform a variety of operations such as changing case, trimming whitespace, splitting and joining strings, repeating strings, replacing substrings, and much more.
For example, you can use the upcase
function to convert a string to uppercase:
iex> String.upcase("hello")
"HELLO"
You can use the downcase
function to convert a string to lowercase:
iex> String.downcase("HELLO")
"hello"
The trim
function allows you to remove leading and trailing whitespace:
iex> String.trim(" hello ")
"hello"
With the split
function, you can divide a string into a list of substrings:
iex> String.split("Hello, World!", ", ")
["Hello", "World!"]
BTW: There is no join
function in the String
module. But you can use Enum.join/2
for that:
iex> Enum.join(["Hello", "World!"], ", ")
"Hello, World!"
The replace
function allows you to substitute a specific pattern in a string with another string:
iex> String.replace("Hello, World!", "World", "Elixir")
"Hello, Elixir!"
These are just a few examples of the many functions available in the String
module. You can find the full list of functions, along with their descriptions and examples, in the official Elixir documentation for the String module at hexdocs.pm/elixir/String.html.
Atom
Atoms in Elixir are constants that are represented by their name. They’re similar to symbols in other languages and start with a :
.
They are extensively used to label or categorize values. For example, when a function might fail, it often returns a tuple tagged with an atom such as {:ok, value}
or {:error, message}
.
iex> :red
:red
iex> :blue
:blue
iex> is_atom(:blue) (1)
true
1 | The function is_atom() checks whether a value is an atom. |
While atoms can be written in snake_case
or CamelCase
, snake_case
is commonly used within the Elixir community. Ensure your atoms are descriptive and indicative of their purpose for code readability.
It’s worth noting that while atoms are handy, they aren’t garbage collected and consume system memory until the system is shut down, so they should be used sparingly. Do not dynamically create atoms from user input or untrusted data, as this can exhaust your system’s available memory and cause it to crash. It is unlikely that you run into this problem but not impossilbe.[2] |
Boolean
Booleans are a data type in Elixir used to represent truthiness and falsiness. They come in two flavors: true
and false
.
Interestingly, in Elixir, booleans are represented as atoms under the hood. Specifically, true
and false
are equivalent to the atoms :true
and :false
, respectively. This means you can use :true
and :false
as if they were true
and false
. But please don’t. It’s generally better to use true
and false
when dealing with booleans, as it makes the code clearer and easier to understand.
Tuple
Tuples in Elixir are a collection of elements enclosed in curly braces {}
. They can hold multiple elements of different types. Tuples are stored contiguously in memory, making data access operations quick. However, modifications (like inserting or deleting elements) can be slow because they require creating a new tuple to preserve immutability.
Tuples are like a fast train with assigned seats. You can quickly find your seat (element), no matter where it is. But if you want to add or remove passengers (modify the tuple), it’s a big deal - you pretty much need to start a new train (create a new tuple). So, tuples are great when you just want to look at your data and don’t plan to change it much. |
Here’s how tuples are represented:
iex> {1, 2, 3} (1)
{1, 2, 3}
iex> {:ok, "test"} (2)
{:ok, "test"}
iex> {true, :apple, 234, "house", 3.14} (3)
{true, :apple, 234, "house", 3.14}
1 | A tuple containing three integers. |
2 | A tuple with an atom representing status and a string — an often used construct in Elixir. |
3 | A tuple containing different data types. |
You can quickly access an element of a tuple by using the elem/2
function:
iex> result = {:ok, "Lorem ipsum"}
{:ok, "Lorem ipsum"}
iex> elem(result, 1) (1)
"Lorem ipsum"
iex> elem(result, 0) (2)
:ok
1 | The elem/2 function provides quick access to tuple elements. |
2 | The index starts from 0 for the first element. |
Tuple Functions
Elixir’s Tuple
module includes various functions for manipulating tuples, such as appending or deleting elements, and converting tuples to lists. Here are some examples:
iex> results = {:ok, "Lorem ipsum"}
{:ok, "Lorem ipsum"}
iex> b = Tuple.append(results, "Test")
{:ok, "Lorem ipsum", "Test"}
iex> c = Tuple.delete_at(b, 1)
{:ok, "Test"}
iex> d = Tuple.insert_at(b, 1, "ipsum")
{:ok, "ipsum", "Lorem ipsum", "Test"}
iex> new_list = Tuple.to_list(d)
[:ok, "ipsum", "Lorem ipsum", "Test"]
iex> tuple_size(d)
4
List
On the other hand, lists, enclosed in brackets []
, are implemented as linked lists, storing each element’s value and a reference to the next element. This structure makes adding elements to the start of the list fast. However, accessing individual elements or determining the list’s length is a linear operation, meaning it can take longer as the list size grows.
Lists are like a chain of people holding hands. Adding a new person at the front of the chain (adding an element to the start of the list) is easy. But if you’re looking for someone specific (accessing a particular element), you have to start at one end of the chain and check each person until you find them. So, lists are excellent when you want to keep adding new elements, but not so great if you frequently need to find a specific element. |
Here’s how you can work with lists:
iex> [1, 2, 3, 4]
[1, 2, 3, 4]
iex> ["a", "b", "c"]
["a", "b", "c"]
iex> [1, "b", true, false, :blue, "house"]
[1, "b", true, false, :blue, "house"]
List concatenation and subtraction can be done using the ++
and --
operators:
iex> [1, 2] ++ [2, 4] (1)
[1, 2, 2, 4]
iex> [1, 2] ++ [1] (2)
[1, 2, 1]
iex> [1, "a", 2, false, true] -- ["a", 2] (3)
[1, false, true]
1 | Appends two lists. |
2 | Adds an element to the list. |
3 | Subtracts elements from a list. |
Working with Lists: Head, Tail, and Other Operations
Elixir offers several built-in functions to operate on lists such as getting the first element (head) and the remaining elements (tail) using hd/1
and tl/1
functions. Also, functions like length/1
provide the list’s size, and various functions in the Enum
and List
modules assist in processing and manipulating lists.
Here are some examples:
iex> shopping_list = ["apple", "orange", "banana", "pineapple"]
["apple", "orange", "banana", "pineapple"]
iex> hd(shopping_list)
"apple"
iex> tl(shopping_list)
["orange", "banana", "pineapple"]
iex> length(shopping_list)
4
iex> numbers = [1, 5, 3, 7, 2, 3, 9, 5, 3]
[1, 5, 3, 7, 2, 3, 9, 5, 3]
iex> Enum.max(numbers)
9
iex> Enum.sort(numbers)
[1, 2, 3, 3, 3, 5, 5, 7, 9]
iex> List.last(shopping_list)
"pineapple"
No need to stress over choosing between lists and tuples early on. As you continue your journey through this book, you’ll develop an intuitive understanding of when to use which based on the specific problem at hand. |
Keyword List
Keyword lists serve as a staple data structure in Elixir, taking the form of lists of tuples which act as key-value pairs, with atoms acting as keys.
Creating Keyword Lists
Typically in Elixir, the most common method to create a keyword list involves using a [key: value]
syntax:
iex> user = [name: "joe", age: 23] (1)
[name: "joe", age: 23]
1 | This syntax offers an intuitive way to create a keyword list, with each atom (e.g., :name , :age ) serving as the key. |
You can access the value associated with a key simply:
iex> user[:name] (1)
"joe"
1 | Use the key, preceded by a colon and within brackets appended to the list name, to retrieve the associated value. |
Keyword lists frequently appear in Phoenix applications, particularly as the final argument in the
|
Alternative Creation Method
Alternatively, although less commonly used, you can create a keyword list by constructing a list of 2-item tuples, with the first item of each tuple being an atom:
iex> user = [{:name, "joe"}, {:age, 23}] (1)
[name: "joe", age: 23]
1 | This list of tuples serves as another representation of a keyword list, equivalent to the more common [key: value] syntax mentioned earlier. |
Manipulating Keyword Lists
Keyword lists can be manipulated with these functions[3]:
Keyword.get/2
: This function retrieves the value associated with a given key within a keyword list.iex> list = [{:a, 1}, {:b, 2}] [a: 1, b: 2] iex> value = Keyword.get(list, :a) 1 iex> IO.puts(value) 1 :ok
Keyword.put/3
: This function is used to either add a new key-value pair to a keyword list or update an existing one.iex> list = [{:a, 1}, {:b, 2}] [a: 1, b: 2] iex> updated_list = Keyword.put(list, :a, 3) [a: 3, b: 2] iex> IO.inspect(updated_list) [a: 3, b: 2] [a: 3, b: 2]
Keyword.delete/2
: This function removes a key-value pair from a keyword list, given its key.iex> list = [{:a, 1}, {:b, 2}] [a: 1, b: 2] iex> updated_list = Keyword.delete(list, :a) [b: 2] iex> IO.inspect(updated_list) [b: 2] [b: 2]
Keyword.merge/2
: This function merges two keyword lists into one. In case of duplicate keys, values from the second list overwrite those from the first.iex> list1 = [{:a, 1}, {:b, 2}] [a: 1, b: 2] iex> list2 = [{:b, 3}, {:c, 4}] [b: 3, c: 4] iex> merged_list = Keyword.merge(list1, list2) [a: 1, b: 3, c: 4] iex> IO.inspect(merged_list) [a: 1, b: 3, c: 4] [a: 1, b: 3, c: 4]
Duplication of Keys
Be aware that keyword lists allow duplication of keys, and this aspect affects how they are manipulated or accessed. For example:
iex> new_user = [name: "fred"] ++ user
[name: "fred", name: "joe", age: 23]
iex> new_user[:name] (1)
"fred"
1 | If duplicate keys are present in a keyword list, a lookup operation retrieves the first occurrence. |
Map
Maps are data structures that store key-value pairs. So they are similar to keyword lists, but with some important differences:
Performance
Maps are faster at finding values for given keys, especially when the map gets large. If you have a big bunch of key-value pairs and need to frequently lookup values, a map is more efficient. Keyword lists can be slower when they grow bigger because they have to search through the list one item at a time.
Key Uniqueness
In a map, each key is unique. If you try to add an entry with a key that already exists, it will just update the existing entry. This is useful when you want to ensure there are no duplicates. With keyword lists, you can have the same key multiple times.
No Key Ordering
Keyword lists keep the order of the elements as you added them, which can be useful in certain situations. Maps, on the other hand, don’t keep track of the insertion order.
Any Key Type
Maps can have keys of any type, while keyword lists usually have atom keys. This gives maps more flexibility if you need to use different types as keys.
Maps are created using the %{}
syntax.
iex(1)> product_prices = %{"Apple" => 0.5, "Orange" => 0.7} (1)
%{"Apple" => 0.5, "Orange" => 0.7}
iex(2)> product_prices["Orange"] (2)
0.7
iex(3)> product_prices["Banana"] (3)
nil
iex(4)> product_prices = %{"Apple" => 0.5, "Apple" => 1, "Orange" => 0.7}
warning: key "Apple" will be overridden in map
iex:4
%{"Apple" => 1, "Orange" => 0.7} (4)
1 | A new map is created and bound to the variable product_prices . |
2 | Value retrieval is straightforward: append the key to the map name within brackets. |
3 | If the given key doesn’t exist, it returns nil . |
4 | Unlike keyword lists, maps disallow duplicate keys. |
Maps are flexible, allowing any data type to serve as both keys and values:
iex> %{"one" => 1, "two" => "abc", 3 => 7, true => "asdf"}
%{3 => 7, true => "asdf", "one" => 1, "two" => "abc"}
Each key must be unique within a map. If there are duplicates, the last one overwrites the previous values. |
Atom Key
Maps support atom keys, enabling some handy features:
iex> product_prices = %{apple: 0.5, orange: 0.7} (1)
%{apple: 0.5, orange: 0.7}
iex> product_prices.apple (2)
0.5
iex> product_prices.banana (3)
** (KeyError) key :banana not found in: %{apple: 0.5, orange: 0.7}
1 | This syntax makes reading and typing easier when using atoms as keys. |
2 | Atom keys allow the use of the dot operator (. ) to access their values. |
3 | If an attempt is made to access a nonexistent key with the dot operator, an error is thrown. |
Sure, let’s break it down a little bit more and explain each part in a simpler way.
Map Functions
Elixir’s Map
module is equipped with various functions that help to perform different operations on maps.[4] Let’s explore the functions I use the most.
Creating a Map
Create a map named product_prices
that stores the prices of various products.
iex> product_prices = %{apple: 0.5, orange: 0.7, coconut: 1}
%{apple: 0.5, coconut: 1, orange: 0.7}
Here, we have three items - apple, orange, and coconut - each with their respective prices.
Converting a Map to a List
The Map.to_list/1
function allows us to convert a map into a keyword list.
iex> Map.to_list(product_prices)
[apple: 0.5, coconut: 1, orange: 0.7]
We can see that our map has now been transformed into a keyword list.
Retrieving All Values from a Map
To fetch all the values from a map (in our case, the product prices), we can utilize the Map.values/1
function.
iex> Map.values(product_prices)
[0.5, 1, 0.7]
This gives us the prices for all our products.
Retrieving All Keys from a Map
To fetch all the keys from a map, we can utilize the Map.keys/1
function.
iex> Map.keys(product_prices)
[:apple, :orange, :coconut]
Splitting a Map
We can split a map into two new maps based on a provided list of keys using the Map.split/2
function.
iex> Map.split(product_prices, [:orange, :apple])
{%{apple: 0.5, orange: 0.7}, %{coconut: 1}}
Our original map is divided into two maps: one containing apple
and orange
, and the other containing coconut
.
Removing a Key-Value Pair from a Map
The Map.delete/2
function can be used when we need to remove a specific key-value pair from our map.
iex> a = Map.delete(product_prices, :orange)
%{apple: 0.5, coconut: 1}
A new map a
is created where the key-value pair for orange
is removed.
Removing Multiple Key-Value Pairs from a Map
For removing multiple key-value pairs, we can use the Map.drop/2
function.
iex> b = Map.drop(product_prices, [:apple, :orange])
%{coconut: 1}
We have removed apple
and orange
, leaving only coconut
in the new map b
.
Merging Two Maps
The Map.merge/2
function enables us to combine two maps.
iex> additional_prices = %{banana: 0.4, pineapple: 1.2}
%{banana: 0.4, pineapple: 1.2}
iex> Map.merge(product_prices, additional_prices)
%{apple: 0.5, banana: 0.4, coconut: 1, orange: 0.7, pineapple: 1.2}
A new map is created that contains the items and their prices from both the maps.
Struct
A struct is a map variant that includes compile-time checks and default values. The defstruct
construct is used to define a struct:
iex> defmodule Product do (1)
...> defstruct name: nil, price: 0 (2)
...> end
iex> %Product{}
%Product{name: nil, price: 0}
iex> apple = %Product{name: "Apple", price: 0.5} (3)
%Product{name: "Apple", price: 0.5}
iex> apple
%Product{name: "Apple", price: 0.5}
iex> apple.price
0.5
iex> orange = %Product{name: "Orange"} (4)
%Product{name: "Orange", price: 0}
1 | Here we define a new struct named Product with the keys name and price . |
2 | Default values are set for the keys. |
3 | A new Product struct is created, setting values for all keys. |
4 | A new Product struct is created with only the name set, leaving the price at its default value. |
Structs ensure that only defined fields can be accessed:
iex> apple.description (1)
** (KeyError) key :description not found in: %Product{name: "Apple", price: 0.5}
iex> banana = %Product{name: "Banana", weight: 0.1} (2)
** (KeyError) key :weight not found
expanding struct: Product.__struct__/1
iex:7: (file)
iex>
1 | Accessing an undefined field, like description in the Product struct, will result in an error. |
2 | Similarly, trying to set an undefined field, such as weight , while creating a new struct will also cause an error. |
As structs are built on top of maps, all map functions are applicable to them. |
Type Conversions in Elixir
In Elixir, type conversions are explicitly invoked by built-in functions. Here are some of the most commonly used functions to convert between different types:
Integer.to_string/1
: This function converts an integer to a string.iex> Integer.to_string(42) "42"
String.to_integer/1
: This function converts a string to an integer. An error is raised if the string does not represent a valid number.iex> String.to_integer("42") 42
Float.to_string/1
andString.to_float/1
: These functions convert between floating-point numbers and strings.iex> Float.to_string(3.14) "3.14" iex> String.to_float("3.14") 3.14
Atom.to_string/1
andString.to_atom/1
: These functions convert between atoms and strings.Note that String.to_atom/1
should be used sparingly because atoms are not garbage-collected, meaning that converting a large amount of unique strings into atoms can exhaust your system memory.iex> Atom.to_string(:elixir) "elixir" iex> String.to_atom("elixir") :elixir
Elixir also provides
Kernel.to_string/1
to convert some terms to a string. For example, lists can be converted to a string representation.iex> to_string([1, 2, 3]) <<1, 2, 3>> (1)
1 This is a so called BitString.
Remember, type conversion in Elixir is explicit and must be invoked through these built-in functions. This design choice, while requiring a bit more typing, can help prevent bugs related to unexpected type conversions.
Operators
In Elixir, operators are special symbols or words that are used to perform various operations like mathematical, comparison, logical and more.
Arithmetic Operators
Arithmetic operators in Elixir allow you to perform basic mathematical calculations. These operators work with numbers and support integer as well as floating-point arithmetic.
Here are the most popular arithmetic operators:
Addition (+): This operator adds two numbers together.
iex> 2 + 2 4
Subtraction (-): This operator subtracts the second number from the first.
iex> 4 - 2 2
Multiplication (*): This operator multiplies two numbers.
iex> 3 * 3 9
Division (/): This operator divides the first number by the second. It always returns a float.
iex> 10 / 2 5.0
If you need integer division, you can use the
div/2
function:iex> div(10, 2) 5
These operators follow standard mathematical precedence rules. If you want to ensure a specific order of operations, use parentheses to make your intentions clear:
iex> 2 + 2 * 3
8
iex> (2 + 2) * 3
12
Elixir’s arithmetic operators are not just for integers and floats; they can also operate on other data types, such as complex numbers and matrices, provided the appropriate libraries are installed. However, the focus here is on their use with numbers.
Comparison Operators
Comparison operators in Elixir are used to compare two values. They evaluate to either true
or false
. These operators play a crucial role in controlling program flow through conditions.
Here are the primary comparison operators:
Less Than (<): This operator returns true if the value on the left is less than the value on the right.
iex> 2 < 3 true
Less Than or Equal To (⇐): This operator returns true if the value on the left is less than or equal to the value on the right.
iex> 2 <= 2 true
Greater Than (>): This operator returns true if the value on the left is greater than the value on the right.
iex> 3 > 2 true
Greater Than or Equal To (>=): This operator returns true if the value on the left is greater than or equal to the value on the right.
iex> 3 >= 3 true
Equal To (==): This operator returns true if the value on the left is equal to the value on the right.
iex> 2 == 2 true
Not Equal To (!=): This operator returns true if the value on the left is not equal to the value on the right.
iex> 2 != 3 true
These operators are typically used in conditional expressions such as those found in if
, unless
, and cond
statements.
It’s important to note that Elixir provides strict comparison operators as well (===
and !==
). These are identical to ==
and !=
respectively, but additionally distinguish between integers and floats.
iex> 2 == 2.0
true
iex> 2 === 2.0
false
Boolean Operators
Boolean operators help to control logical flow within your program. They perform operations on boolean values and return a boolean result (true
or false
). The primary boolean operators include and
, or
, not
, and xor
.
and
: Theand
operator returns true if both the operands are true. Otherwise, it returns false.iex> true and true true iex> true and false false
or
: Theor
operator returns true if at least one of the operands is true.iex> false or true true iex> false or false false
not
: Thenot
operator returns the opposite boolean value of the operand.iex> not true false iex> not false true
xor
: Thexor
(exclusive or) operator returns true if exactly one of the operands is true.iex> true xor false true iex> true xor true false
Short-Circuit Operators
In addition to the boolean operators and
, or
, and not
, Elixir provides &&
, ||
, and !
as equivalent short-circuit operators. Short-circuit evaluation, also known as minimal evaluation, is a method of evaluation in which the second argument is executed or evaluated only if the first argument does not suffice to determine the value of the expression.
However, these operators handle non-boolean values differently than their counterparts, which is important to understand to avoid unexpected behavior in your Elixir programs.
&&
operator: This operator returns the first value if it is falsy (eitherfalse
ornil
). Otherwise, it returns the second value. This is why it only evaluates the second argument if the first one is truthy.iex> nil && true nil
In the above example,
nil && true
returnsnil
becausenil
is a falsy value.||
operator: This operator returns the first value if it is truthy. Otherwise, it returns the second value. It only evaluates the second argument if the first one is falsy.iex> true || "Hello" true
In this example,
true || "Hello"
returnstrue
becausetrue
is a truthy value.!
operator: This operator negates the truthiness of the value. It returnsfalse
for all truthy values andtrue
for all falsy values.iex> !1 false
Here,
!1
returnsfalse
because1
is considered a truthy value in Elixir.=== String Concatenation Operator
<>
In Elixir, the <>
operator takes two strings and merges them together, forming a new string. This process is commonly known as "string concatenation." Here’s a simple example:
iex> "Hello" <> " world"
"Hello world"
In the above example, "Hello"
and " world"
are two separate strings. The <>
operator combines them into one string: "Hello world"
.
In practice, the <>
operator is extensively used when there’s a need to dynamically construct a string. It allows parts of the string to be variables that can change depending on the program’s context:
iex> greeting = "Hello"
"Hello"
iex> name = "Alice"
"Alice"
iex> greeting <> ", " <> name
"Hello, Alice"
In the example above, greeting
and name
are variables holding different string values. Using the <>
operator, we can concatenate these variables with another string (", ") to create the desired output.
It’s important to note that the <>
operator only works with strings. Attempting to concatenate a non-string data type without first converting it to a string will result in an error:
iex> "The answer is " <> 42
** (ArgumentError) "argument error"
To avoid such errors, ensure all operands of the <>
operator are strings. For example, you can use the Integer.to_string/1
function to convert an integer to a string:
iex> "The answer is " <> Integer.to_string(42)
"The answer is 42"
Interpolation Operator #{}
The interpolation operator in Elixir, represented as #{}
, is a powerful tool used for inserting values within a string. It allows for dynamic expression of values within a string without the need for explicit concatenation. Here’s a simple example:
iex> name = "Alice"
"Alice"
iex> "Hello, #{name}"
"Hello, Alice"
In the example above, the variable name
is interpolated into the string. The resulting string is "Hello, Alice"
.
String interpolation in Elixir can handle more complex expressions, not just variables. This includes arithmetic operations, function calls, and more:
iex> number = 5
5
iex> "The square of #{number} is #{number * number}"
"The square of 5 is 25"
In the above code, the expressions inside the interpolation operator #{}
are evaluated, and their results are automatically converted into strings and inserted in the appropriate place.
Another powerful aspect of string interpolation in Elixir is that it’s not restricted to strings. It can handle any data type that can be meaningfully converted into a string representation, including integers, floats, atoms, lists, and even tuples:
iex> tuple = {1, 2, 3}
{1, 2, 3}
iex> "The tuple is #{tuple}"
"The tuple is {1, 2, 3}"
In the above example, the tuple is automatically converted to its string representation and inserted into the string.
Remember, the expressions inside the interpolation operator #{}
must be valid Elixir expressions.
The #{} operator itself can’t be used outside a string, as it’s a part of the string syntax, not a standalone operator. |
The Pipe Operator (|>)
The pipe operator |>
is an effective tool in enhancing the readability of your code. Referred to as syntactic sugar, it directs the output from the expression to its left as the first argument into the function on its right. It thus allows for a clean and streamlined way to chain multiple functions together.
It is easier than it sounds. The following code examples explain it.
Consider a case where you wish to reverse a string with String.reverse/1
and subsequently capitalize it using String.capitalize/1
. Traditionally, you might go about it as follows:
iex> String.reverse("house") (1)
"esuoh"
iex> String.capitalize("esuoh") (2)
"Esuoh"
iex> String.capitalize(String.reverse("house")) (3)
"Esuoh"
1 | String.reverse/1 function reverses the string. |
2 | String.capitalize/1 function capitalizes the first letter of a string. |
3 | Both functions are integrated to first reverse and then capitalize the string. |
Although String.capitalize(String.reverse("house"))
is technically correct, it can be a bit difficult to read. This is where the pipe operator |>
comes in handy:
iex> "house" |> String.reverse() |> String.capitalize() (1)
"Esuoh"
1 | The pipe operator |> passes the result of the first function as the first parameter to the subsequent function. |
Moreover, the pipe operator can be seamlessly chained for multiple operations:
iex> "house" |> String.reverse() |> String.capitalize() |> String.slice(0, 3)
"Esu"
Employing the pipe operator, the code becomes more legible, easier to understand, and more maintainable. The benefits of this operator are particularly noticeable in multi-line source code where each transformation is clearly outlined:
example =
"house"
|> String.reverse()
|> String.capitalize()
|> String.slice(0, 3)
This presentation enhances clarity and readability of the code, allowing for better understanding and maintenance.
The Match Operator =
(Pattern Matching)
In various programming languages, the equals sign (=
) signifies assignment. For example, x = 5
generally reads as "Let x
be equal to 5." Elixir, however, assigns a slightly different role to the equals sign.
In Elixir, =
acts as the _match operator. Trying to match the right side of the expression with its left side. When we say x = 5
in Elixir, we are essentially telling the language, "Match x
to the value 5." If x
is unbound or has no assigned value, Elixir will bind x
to 5, making it behave similarly to an assignment operator. But if x
already holds a value, Elixir will attempt to rebind it. Older versions of Elixir did throw an error in this case, but this behavior was changed for a better user experience.
iex> x = 5
5
Elixir’s pattern matching becomes even more robust with more complex data types like tuples or lists:
iex> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> a
:hello
iex> b
"world"
iex> c
42
In the above snippet, Elixir matches variables a
, b
, and c
to their respective values in the right-side tuple. Thus a
equals :hello
, b
equals "world"
, and c
equals 42
.
While it might seem similar to destructuring in JavaScript or other languages, remember that Elixir aims to match instead of simply assigning. If no match is found, Elixir throws an error:
iex> {d, e, f} = {:hi, "there", 23}
{:hi, "there", 23}
iex> {d, e} = {:hi, "there", 23}
** (MatchError) no match of right hand side value: {:hi, "there", 23}
In the second command, a two-element pattern cannot match a three-element tuple, thus resulting in a MatchError
.
To summarize, pattern matching in Elixir verifies whether a certain pattern matches your data. If it does, Elixir assigns values to variables based on that pattern.
Pattern Matching is an incredibly powerful concept in Elixir, used in myriad ways. This is merely an introduction—we will explore more examples throughout this book. |
Functions
Pattern matching is pervasive in Elixir. It is used with functions too:
iex> defmodule Area do
...> def circle(:exact, radius) do (1)
...> 3.14159265359 * radius * radius
...> end
...>
...> def circle(:normal, radius) do (2)
...> 3.14 * radius * radius
...> end
...>
...> def circle(radius) do (3)
...> circle(:normal, radius)
...> end
...> end
iex> Area.circle(:exact, 4)
50.26548245744
iex> Area.circle(:normal, 4)
50.24
iex> Area.circle(4)
50.24
1 | We define a circle/2 function which matches if the first argument is the atom :exact . |
2 | We define a circle/2 function which matches if the first argument is the atom :normal . |
3 | We define a circle/1 function which calls the circle/2 function with the :normal argument. |
Functions with Guards
Guards add extra layers to pattern matching with functions. Full details can be found at https://hexdocs.pm/elixir/guards.html. Let’s look at a few examples. Guards start with when
:
iex> defmodule Law do
...> def can_vote?(age) when is_integer(age) and age > 17 do (1)
...> true
...> end
...>
...> def can_vote?(age) when is_integer(age) do (2)
...> false
...> end
...>
...> def can_vote?(_age) do (3)
...> raise ArgumentError, "age should be an integer"
...> end
...> end
iex> Law.can_vote?(18)
true
iex> Law.can_vote?(16)
false
iex> Law.can_vote?("18")
** (ArgumentError) age should be an integer
1 | We define a can_vote?/1 function with a guard clause that checks whether the age is an integer and greater than 17. |
2 | We define a can_vote?/1 function with a guard clause that checks whether the age is an integer. |
3 | We define a can_vote?/1 function to handle other cases. |
Pattern Matching With Various Data Structures
Pattern matching extends to various data structures in Elixir, including lists, maps, strings, and even function clauses. Let’s see how this works.
Lists
Elixir provides a unique syntax for pattern matching the head and tail of a list. Let’s consider the following examples:
iex> shopping_list = ["apple", "orange", "banana", "pineapple"] (1)
["apple", "orange", "banana", "pineapple"]
iex> [head | tail] = shopping_list (2)
["apple", "orange", "banana", "pineapple"]
iex> head
"apple"
iex> tail
["orange", "banana", "pineapple"]
iex> [a | b] = tail (3)
["orange", "banana", "pineapple"]
iex> a
"orange"
iex> b
["banana", "pineapple"]
iex> [first_product, second_product | tail] = shopping_list (4)
["apple", "orange", "banana", "pineapple"]
iex> first_product
"apple"
iex> second_product
"orange"
iex> tail
["banana", "pineapple"]
iex> [first_product | [second_product | tail]] = shopping_list (5)
["apple", "orange", "banana", "pineapple"]
1 | We match a list to the variable shopping_list . |
2 | [head | tail] is the special syntax to match a head and tail of a given list. |
3 | Here we match the head a and the tail b with tail . |
4 | This is slightly more complex. We match the first and second product followed by a tail. |
5 | This alternative syntax yields the same result but follows different logic. Choose the one you prefer. |
If we know that a list has a specific number of elements, we can match it directly:
iex> shopping_list = ["apple", "orange", "banana", "pineapple"]
["apple", "orange", "banana", "pineapple"]
iex> [a, b, c, d] = shopping_list
["apple", "orange", "banana", "pineapple"]
iex> a
"apple"
iex> b
"orange"
iex> [e, f, g] = shopping_list (1)
** (MatchError) no match of right hand side value: ["apple", "orange", "banana", "pineapple"]
1 | Just checking. You get an MatchError if Elixir can’t match both sides. |
Keyword Lists
In Elixir, pattern matching on keyword lists is no longer recommended due to strict ordering and count requirements introduced in version 1.15. While pattern matching was once possible, recent versions of Elixir make it unreliable. Keyword lists are often used for optional arguments where keys may be omitted, making pattern matching impractical.
Instead of pattern matching, access values directly by key using the [] syntax:
iex> list = [a: 1, b: 2, c: 3]
[a: 1, b: 2, c: 3]
iex> list[:a]
1
iex> list[:c]
3
Matching Inside Functions
Pattern matching with keyword lists is often used in function heads. Consider a system where you want to provide different messages to users based on their role. You could achieve this with pattern matching on keyword lists:
defmodule User do
def greet(name, opts \\ []) (1)
def greet(name, [role: "admin"]) do
"Welcome, #{name}. You have admin privileges."
end
def greet(name, [role: "moderator"]) do
"Welcome, #{name}. You can moderate content."
end
def greet(name, []) do
"Welcome, #{name}."
end
end
IO.puts User.greet("Alice") # Outputs: "Welcome, Alice."
IO.puts User.greet("Bob", role: "admin") # Outputs: "Welcome, Bob. You have admin privileges."
IO.puts User.greet("Carol", role: "moderator") # Outputs: "Welcome, Carol. You can moderate content."
1 | We define a greet/2 function header with a default value for the second argument. The default value is an empty list [] . |
In this example, we define different greetings based on user roles. When calling the greet
function, we can optionally provide a role
.
Maps
Matching a map in Elixir differs slightly from tuples or lists. We can match specific values we’re interested in:
iex> product_prices = %{apple: 0.5, orange: 0.7, pineapple: 1}
%{apple: 0.5, orange: 0.7, pineapple: 1}
iex> %{orange: price} = product_prices (1)
%{apple: 0.5, orange: 0.7, pineapple: 1}
iex> price
0.7
iex> %{orange: price1, apple: price2} = product_prices (2)
%{apple: 0.5, orange: 0.7, pineapple: 1}
iex> price1
0.7
iex> price2
0.5
1 | Here we match just one value. |
2 | We can match multiple values. It’s not necessary to match the entire map. |
Strings
Pattern matching with strings is best illustrated with a code snippet:
iex> user = "Stefan Wintermeyer"
"Stefan Wintermeyer"
iex> "Stefan " <> last_name = user
"Stefan Wintermeyer"
iex> last_name
"Wintermeyer"
The left side of a <> operator in a match should always be a string. Otherwise, Elixir can’t determine its size. |
Wildcards
Sometimes you want to pattern match something but you don’t care about the value. By using the _
wildcard, either standalone or as a prefix to a variable name, you signal to Elixir that there’s no requirement for a binding to a particular variable. Here are two examples:
iex(1)> cart = {"apple", "orange", "banana"}
{"apple", "orange", "banana"}
iex(2)> {first, _, _} = cart (1)
{"apple", "orange", "banana"}
iex(3)> IO.puts(first)
"apple"
iex(4)> cart2 = ["apple", "orange", "banana", "pineapple"]
["apple", "orange", "banana", "pineapple"]
iex(5)> [head | _tail] = cart2 (2)
["apple", "orange", "banana", "pineapple"]
iex(6)> IO.puts(head)
"apple"
1 | We use wildcards _ to ignore "orange" and "banana" in the cart tuple while pattern matching the first item to first . |
2 | With the list cart2 , we pattern match the first item to head , ignoring the rest of the list by prefixing _ to tail . |
Using tail instead of just increases the readability of the code. |
The Range Operator
The range operator (..
) in Elixir introduces a convenient way of defining sequences of successive integers. This section will explore the range operator, demonstrating its various uses from simple range definitions to utilization in functions for processing sequences.
Understanding the Range Operator ..
In Elixir, a range is created by two integers separated by the ..
operator. This sequence includes both the start and end points. For example, 1..5
creates a range of integers from 1 to 5 inclusive. 5..1
does the same but in reverse order.
Ranges in Elixir are considered as enumerables, which means they can be used with the Enum
module to iterate over the sequence of numbers. This capability makes the range operator a versatile tool in various situations involving sequences of numbers.
iex> 1..5
1..5
iex> Enum.to_list(1..5)
[1, 2, 3, 4, 5]
In the code above, the first command creates a range from 1 to 5. The second command converts the range into a list using the Enum.to_list/1
function.
Range Operator in Functions
The range operator can also be used in combination with functions:
iex> Enum.map(1..5, fn x -> x * x end)
[1, 4, 9, 16, 25]
In this example, the Enum.map
function is used to square each number in the range 1..5
, resulting in a new list [1, 4, 9, 16, 25]
.
Step in Ranges
Elixir also allows you to define the step (increment) between successive numbers in a range using the //
operator:
iex> Enum.to_list(1..11//3)
[1, 4, 7, 10]
In this example, 1..11//3
creates a range from 1 to 11 with a step of 3, resulting in the list [1, 4, 7, 10]
.
Capture Operator &
The Capture operator, denoted as &
, is a unique feature in Elixir that enables the creation of anonymous functions, often in a more succinct and readable way than the traditional fn → end
syntax.
The Capture operator is often used to create quick, inline functions. Here’s a simple example:
iex> add = &(&1 + &2)
#Function<12.128620087/2 in :erl_eval.expr/5>
iex> add.(1, 2)
3
In the above example, &(&1 + &2)
creates an anonymous function that adds two arguments together. The placeholders &1
and &2
refer to the first and second arguments, respectively. The function is then assigned to the variable add
, and it can be invoked with add.(1, 2)
.
The Capture operator isn’t just for simple functions. It can be used with more complex expressions and even function calls:
iex> double = &(&1 * 2)
#Function<6.128620087/1 in :erl_eval.expr/5>
iex> double.(10)
20
In the above example, &(&1 * 2)
creates an anonymous function that doubles its argument.
You can also use the Capture operator to reference named functions from modules. For example, to reference the length
function from the List module:
iex> len = &length/1
&:erlang.length/1
iex> len.([1, 2, 3, 4, 5])
5
In the example above, &length/1
captures the length
function from the which takes one argument (/1
). This function is then assigned to the variable len
.
Cons Operator |
in Elixir
The Cons operator, represented by the pipe character (|
), is a core tool in Elixir used to construct and manipulate lists.
Understanding the Cons Operator |
In Elixir, lists are fundamentally built as singly linked lists. That means each element in the list holds its value and also the remainder of the list. These individual pieces are referred to as the "head" and the "tail" of the list. The Cons operator is used to combine a head and a tail into a list.
Here is a simple example:
iex> list = [1 | [2 | [3 | []]]]
[1, 2, 3]
In this example, the expression [1 | [2 | [3 | []]]]
constructs a list with the elements 1, 2, and 3. The last list in the chain is an empty list []
.
Using the Cons Operator |
One common usage of the Cons operator is in pattern matching to destructure a list into its head and tail:
iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]
In the example above, [head | tail] = [1, 2, 3]
splits the list [1, 2, 3]
into the head (the first element, 1
) and the tail (the remainder of the list, [2, 3]
).
This operator is particularly useful when working with recursive functions that need to operate on each element of a list.
Caution when Using the Cons Operator |
While the Cons operator can be used to construct lists, it should be noted that the resulting data structure is only properly recognized as a list if the tail is a list or an empty list. For example:
iex> ["Hello" | "World"]
["Hello"| "World"]
In this case, since "World"
is not a list, Elixir does not treat the entire structure as a regular list.
Logical Expressions
The boolean
type in Elixir can be either true
or false
. You can use logical operators like and
, or
, and not
to manipulate these boolean values:
iex> true and true
true
iex> true or false
true
iex> not true
false
Elixir’s and
, or
, and not
operators strictly work with boolean values. But there’s more! Elixir also provides &&
(and), ||
(or), and !
(not) operators that can handle truthy and falsy values, giving them a bit of flexibility. In Elixir, every value is considered truthy except for false
and nil
, which are falsy.
To clarify:
&&
(and) returns the first falsy value or the last value if all are truthy.||
(or) returns the first truthy value or the last value if all are falsy.!
(not) returnsfalse
if its argument is truthy andtrue
if it’s falsy.
Let’s consider a few examples:
iex> true && :hello
:hello
iex> false || "world"
"world"
iex> !nil
true
In the first example, :hello
is returned because true && :hello
evaluates to the last truthy value, which is :hello
. In the second example, "world"
is returned because false || "world"
evaluates to the first truthy value, which is "world"
. In the final example, !nil
gives true
because nil
is a falsy value and !
flips it to true
.
Enumerables
An enumerable is any data structure that can be traversed or iterated over. It can be lists like [1, 2, 3]
, maps like %{a: 1, b: 2}
and ranges like 1..3
. All these structures can be processed using the functions provided by the Enum module.
Introduction
The Enum
module contains functions for mapping, filtering, grouping, sorting, reducing, and other operations. They are the building blocks for manipulating and transforming data collections in a functional and declarative style, enhancing code readability and maintainability.
Consider this example of using Enum
to multiply each element in a list by 2:
list = [1, 2, 3, 4]
Enum.map(list, fn x -> x * 2 end)
# => [2, 4, 6, 8]
The Enum.map
function takes two arguments: the enumerable (list in this case) and a transformation function for each element.
For further enhancement, Elixir’s pipe operator (|>
) can be used with Enum
functions for cleaner and more readable code. Here’s an example:
list = [1, 2, 3, 4]
list
|> Enum.map(fn x -> x * 3 end)
|> Enum.filter(fn x -> rem(x, 2) == 0 end)
# => [6, 12]
This statement takes a list, multiplies each element by 3 using Enum.map
, and then filters out the odd numbers using Enum.filter
. The use of the pipe operator makes the code flow naturally and easier to read.
You can also use the
|
Enum functions are eager; they execute immediately and return a result. If memory usage is a concern with very large collections, consider using the Stream module for lazy computation. |
Commonly Used Enum Functions
Enum offers a ton of useful functions. All are listed at the official Enum documentation. Here are some of the most commonly used functions to give you an idea of what’s available.
Enum.map/2
The Enum.map/2
function is used to transform each element in an enumerable using a provided function.
list = [1, 2, 3, 4]
Enum.map(list, fn x -> x * 2 end)
# => [2, 4, 6, 8]
The &1
shorthand can be used as follows:
list = [1, 2, 3, 4]
list |> Enum.map(&(&1 * 2))
# => [2, 4, 6, 8]
More details can be found at the official Elixir Enum.map/2 documentation.
Enum.filter/2
The Enum.filter/2
function filters out elements based on a provided function.
list = [1, nil, 2, nil, 3]
Enum.filter(list, fn x -> x != nil end)
# => [1, 2, 3]
Using the &1
shorthand:
list = [1, nil, 2, nil, 3]
list |> Enum.filter(&(&1 != nil))
# => [1, 2, 3]
More details can be found at the official Elixir Enum.filter/2 documentation.
Enum.reduce/2,3
The Enum.reduce/2,3
function reduces an enumerable to a single value.
list = [1, 2, 3, 4]
Enum.reduce(list, 0, fn x, acc -> x + acc end)
# => 10
The use of reduce/3 and it’s accumulator is similar to the fold function in other languages. It can be tricky to use. |
More details can be found at the official Elixir Enum.reduce/2 documentation.
Enum.sort/1,2
The Enum.sort/1,2
function sorts the elements in an enumerable.
list = [4, 2, 3, 1]
Enum.sort(list)
# => [1, 2, 3, 4]
You can provide a comparator function:
list = [4, 2, 3, 1]
Enum.sort(list, fn a, b -> a > b end)
# => [4, 3, 2, 1]
More details can be found at the official Elixir Enum.sort/2 documentation.
Enum.at/2,3
Returns the element at the given index
(zero based) or a default value.
list = [1, 2, 3, 4]
Enum.at(list, 2)
# Output: 3
More details can be found at the official Elixir Enum.at/2,3 documentation.
Enum.concat/1,2
Concatenates the collection of enumerable(s) given.
Enum.concat([[1, 2], [3, 4]])
# Output: [1, 2, 3, 4]
More details can be found at the official Elixir Enum.concat/1,2 documentation.
Enum.count/1,2
Counts the enumerable items, optionally, using the provided function.
list = [1, 2, 3, 4]
Enum.count(list)
# Output: 4
More details can be found at the official Elixir Enum.count/1,2 documentation.
Enum.find/2,3
Finds the first element for which the provided function returns a truthy value.
list = [1, 2, 3, 4]
Enum.find(list, fn x -> x > 2 end)
# Output: 3
More details can be found at the official Elixir Enum.find/2,3 documentation.
Enum.group_by/2,3
Groups all items in the enumerable by the given function.
list = [{:apple, "fruit"}, {:carrot, "vegetable"}, {:banana, "fruit"}]
Enum.group_by(list, fn {_name, type} -> type end)
# Output: %{"fruit" => [{:apple, "fruit"}, {:banana, "fruit"}], "vegetable" => [{:carrot, "vegetable"}]}
More details can be found at the official Elixir Enum.group_by/2,3 documentation.
Enum.join/1,2
Joins all the items in the enumerable into a single string.
list = ["Hello", "World"]
Enum.join(list, " ")
# Output: "Hello World"
More details can be found at the official Elixir Enum.join/1,2 documentation.
Enum.max/1
Returns the maximum value in the enumerable.
list = [1, 2, 3,
4]
Enum.max(list)
# Output: 4
More details can be found at the official Elixir Enum.max/1 documentation.
Enum.min/1
Returns the minimum value in the enumerable.
list = [1, 2, 3, 4]
Enum.min(list)
# Output: 1
More details can be found at the official Elixir Enum.min/1 documentation.
Enum.random/1
Selects a random element from the enumerable.
list = [1, 2, 3, 4]
Enum.random(list)
# Output: Random value from the list
More details can be found at the official Elixir Enum.random/1 documentation.
Enum.reject/2
Filters out the items in the enumerable for which the provided function returns a truthy value.
list = [1, 2, 3, 4]
Enum.reject(list, fn x -> x < 3 end)
# Output: [3, 4]
More details can be found at the official Elixir Enum.reject/2 documentation.
Enum.sum/1
Returns the sum of all items in the enumerable.
list = [1, 2, 3, 4]
Enum.sum(list)
# Output: 10
More details can be found at the official Elixir Enum.sum/1 documentation.
Introduction
The Stream
module contains functions similar to the Enum
module for mapping, filtering, grouping, sorting, reducing, and other operations. However, the key difference is that Stream
operations are lazy. This means they only compute results when necessary, potentially saving a lot of resources when dealing with large data sets or I/O operations.
Consider this example of using Stream
to multiply each element in a list by 2:
list = [1, 2, 3, 4]
Stream.map(list, fn x -> x * 2 end)
# => #Stream<[enum: [1, 2, 3, 4], funs: [#Function<45.122072036/1 in Stream.map/2>]]>
You might notice that the output is not a list but a Stream
. To retrieve the final result, you will need to convert the stream back into a list:
list = [1, 2, 3, 4]
list
|> Stream.map(fn x -> x * 2 end)
|> Enum.to_list()
# => [2, 4, 6, 8]
The Stream
module can be used with Elixir’s pipe operator (|>
) to create a sequence of transformations. The transformations only get executed when the stream is converted into a list or another enumerable.
This is how you would use Stream
to multiply each element by 3 and then filter out odd numbers:
list = [1, 2, 3, 4]
list
|> Stream.map(fn x -> x * 3 end)
|> Stream.filter(fn x -> rem(x, 2) == 0 end)
|> Enum.to_list()
# => [6, 12]
Commonly Used Stream Functions
Stream offers a lot of useful functions, similar to Enum. All are listed at the official Stream documentation. Here are some of the most commonly used functions to give you an idea of what’s available.
Stream.map/2
The Stream.map/2
function is used to transform each element in an enumerable using a provided function. The result is a new Stream that can be evaluated later.
list = [1, 2, 3, 4]
list
|> Stream.map(fn x -> x * 2 end)
|> Enum.to_list()
# => [2, 4, 6, 8]
The &1
shorthand can be used as follows:
list = [1, 2, 3, 4]
list
|> Stream.map(&(&1 * 2))
|> Enum.to_list()
# => [2, 4, 6, 8]
More details can be found at the official Elixir Stream.map/2 documentation.
Stream.filter/2
The Stream.filter/2
function filters out elements based on a provided function, resulting in a new Stream.
list = [1, nil, 2, nil, 3]
list
|> Stream.filter(fn x -> x != nil end)
|> Enum.to_list()
# => [1, 2, 3]
Using the &1
shorthand:
list = [1, nil, 2, nil, 3]
list
|> Stream.filter(&(&1 != nil))
|> Enum.to_list()
# => [1, 2, 3]
More details can be found at the official Elixir Stream.filter/2 documentation.
Stream.reduce/2,3
The Stream.reduce/2,3
function reduces an enumerable to a single value.
list = [1, 2, 3, 4]
Stream.reduce(list, 0, fn x, acc -> x + acc end)
# => 10
The use of reduce/3 and it’s accumulator is similar to the fold function in other languages. It can be tricky to use. |
More details can be found at the official Elixir Stream.reduce/2 documentation.
Stream.take/2
The Stream.take/2
function generates a new stream that takes the first n
items from the original stream.
list = [1, 2, 3, 4]
list
|> Stream.take(2)
|> Enum.to_list()
# => [1, 2]
More details can be found at the official Elixir Stream.take/2 documentation.
Stream.drop/2
The Stream.drop/2
function generates a new stream that drops the first n
items from the original stream.
list = [1, 2, 3, 4]
list
|> Stream.drop(2)
|> Enum.to_list()
# => [3, 4]
More details can be found at the official Elixir Stream.drop/2 documentation.
Stream.concat/1,2
The Stream.concat/1,2
function generates a new stream that concatenates two streams or a stream of streams.
Stream.concat([1, 2], [3, 4])
|> Enum.to_list()
# => [1, 2, 3, 4]
More details can be found at the official link:https://hex
docs.pm/elixir/Stream.html#concat/2[Elixir Stream.concat/1,2 documentation].
Stream.cycle/1
The Stream.cycle/1
function generates an infinite stream repeating the given enumerable.
Stream.cycle([1, 2])
|> Stream.take(5)
|> Enum.to_list()
# => [1, 2, 1, 2, 1]
More details can be found at the official Elixir Stream.cycle/1 documentation.
Stream.unzip/1
The Stream.unzip/1
function generates two new streams from a stream of tuples.
list = [{1, "a"}, {2, "b"}, {3, "c"}]
{left, right} = Stream.unzip(list)
Enum.to_list(left)
# => [1, 2, 3]
Enum.to_list(right)
# => ["a", "b", "c"]
More details can be found at the official Elixir Stream.unzip/1 documentation.
Enum vs Stream
Think about Enum
and Stream
as two different chefs in a kitchen who are asked to prepare a large meal.
The Enum Chef (Eager Chef):
The Enum chef is eager to get the job done. He tries to cook everything at once. He gets every ingredient, every pot and pan, and starts cooking immediately. This is great if you’re not cooking a lot of food, because everything gets done fast.
But what if the meal or the number of meals is huge? Well, then the Enum chef might run into trouble. His kitchen (or our computer’s memory) might not have enough room for all the food he’s trying to cook at once. He might get overwhelmed because he’s trying to do too much at once.
The Stream Chef (Lazy Chef):
The Stream chef, on the other hand, is more laid-back. He doesn’t start cooking until it’s absolutely necessary. He prepares each dish one at a time, using only the ingredients and cookware needed for each dish. Once a dish is ready, he moves on to the next one.
If the meal is huge, the Stream chef handles it better because he only works on one dish at a time, which means he doesn’t need a lot of room in his kitchen. He’s more efficient with large meals because he can handle them piece by piece.
Comparing the Chefs:
Speed: The Enum chef (Eager chef) works faster when the meal is small because he cooks everything at once. But the Stream chef (Lazy chef) could be faster for large meals because he efficiently handles them one dish at a time. You could use a stopwatch to see who finishes cooking first.
Kitchen Space (Memory): The Stream chef (Lazy chef) uses his kitchen space more efficiently because he only prepares one dish at a time. This difference becomes obvious when they’re asked to prepare a large meal. You could look at how messy their kitchens are to see who uses space better.
So, when you’re choosing between Enum and Stream in your Elixir code, think about the size of your "meal" (your data), and pick the chef that suits your needs best.
Sigils
Sigils are another way of representing literals. A literal is a notation for representing a fixed value in source code. Sigils start with a tilde (~
) character, which is followed by a letter, and then there is some content surrounded by delimiters. There are 8 different delimiters (having different delimiters means that you can choose one which reduces the need to escape characters in the content).
~s/example text/
~s|example text|
~s"example text"
~s'example text'
~s(example text)
~s[example text]
~s{example text}
~s<example text>
In the following sections, we will explore some of the most commonly used sigils in Elixir: ~s
for strings, ~r
for regular expressions, ~w
for word lists, and those for date/time structs. It is also possible for you to create your own sigils.
The ~s and ~S Sigils
The ~s
and ~S
sigils in Elixir are used for creating strings.
Let’s look at some examples of using the ~s
sigil:
iex> ~s(Hello, my friend!) (1)
"Hello, my friend!"
iex> ~s(He said, "I hope you are well") (2)
"He said, \"I hope you are well\""
iex> ~s/Hello (Goodbye)/ (3)
"Hello (Goodbye)"
1 | In this case, we use the () delimiters. |
2 | We do not need to escape the double quotes (you will see that they are escaped in the output). |
3 | By changing the delimiters, we do not need to escape the parentheses. |
The ~S
(uppercase) sigil also creates a string, but does not support interpolation:
iex> ~s(1 + 1 = #{1 + 1})
"1 + 1 = 2" (1)
iex> ~S(1 + 1 = #{1 + 1})
"1 + 1 = \#{1 + 1}" (2)
1 | The result of 1 + 1 is returned instead of #{1 + 1} . |
2 | The content is returned as it is written, with no interpolation. |
The ~r Sigil - Regular expressions
~r
is the sigil used to represent a regular expression:
iex> regex = ~r/bcd/
~r/bcd/
iex> "abcde" =~ regex
true
iex> "efghi" =~ regex
false
As you can see, the ~r
sigil allows you to easily create regular expressions in Elixir. It checks if the given string contains the regular expression pattern.
Modifiers are supported to change the behavior of the regular expressions. Two examples:
i
: Makes the regular expression case-insensitive.m
: Causes ^ and $ to mark the beginning and end of each line. Use \A and \z to match the end or beginning of the string.
Here is an example of using the i
modifier:
iex> regex = ~r/stef/i (1)
~r/stef/i
iex> "Stefan" =~ regex
true
1 | The i modifier makes the regular expression case-insensitive, so "stef" will match "Stefan". |
For a complete list of modifiers, have a look at the Regex module documentation.
The ~w Sigil
The ~w
sigil in Elixir helps to create a list of words without the need for quotes around each word. You start with ~w(
, then put your words separated by spaces, and finish with )
.
Here is an example of how it is used:
iex> words = ~w(hello world this is Elixir)
["hello", "world", "this", "is", "Elixir"]
As you can see, it turns the words separated by spaces into a list of strings.
Modifiers
For the ~w
sigil, you can add a c
, s
, or a
after the w
, changing the type of the output.
c
makes the elements character lists (charlists).s
makes the elements strings.a
makes the elements atoms.
Here are some examples:
iex> ~w(hello world this is Elixir)c
[~c"hello", ~c"world", ~c"this", ~c"is", ~c"Elixir"]
iex> ~w(hello world this is Elixir)s
["hello", "world", "this", "is", "Elixir"]
iex> ~w(hello world again)a
[:hello, :world, :again]
Also note that you can use different delimiters for your ~w
sigil, not just parentheses. For example, ~w{hello world this is Elixir}
, ~w/hello world this is Elixir/
and ~w|hello world this is Elixir|
are all valid.
Date and Time
Elixir provides several date / time structs which all have their own sigils. These include the ~D
sigil for dates, ~T
for times, ~N
for naive date-times, and ~U
for UTC date-times.
You can find more information about timezones and DateTime at https://hexdocs.pm/elixir/DateTime.html
Date
Elixir provides a %Date{}
struct that contains the fields year
, month
, day
and calendar
.
With the ~D
sigil, you can create a new %Date{}
struct:
iex> birthday = ~D[1973-03-23]
~D[1973-03-23]
iex> birthday.day
23
iex> birthday.month
3
iex> birthday.year
1973
iex> Date.utc_today()
~D[2020-09-23] (1)
1 | The return value for many of the functions in the Date module use the ~D sigil. |
Time
There is a %Time{}
struct that contains the fields hour
, minute
, second
, microsecond
and calendar
.
With the ~T
sigil, you can create a new %Time{}
struct:
iex> now = ~T[09:29:00.0]
~T[09:29:00.0]
iex> now.hour
9
iex> Time.utc_now()
~T[04:57:25.658722] (1)
1 | The return value for many of the functions in the Time module use the ~T sigil. |
NaiveDateTime
The %NaiveDateTime{}
struct is a combination of %Date{}
and %Time{}
.
With the ~N
sigil, you can create a new %NaiveDateTime{}
struct:
iex> timestamp = ~N[2020-05-08 09:48:00]
~N[2020-05-08 09:48:00]
DateTime
The %DateTime{}
struct adds timezone information to a %NaiveDateTime{}
.
You can create a new %DateTime{}
struct with the ~U
sigil:
iex> timestamp = ~U[2029-05-08 09:59:03Z]
~U[2029-05-08 09:59:03Z]
iex> DateTime.utc_now()
~U[2020-09-23 04:58:22.403482Z] (1)
1 | The return value for many of the functions in the DateTime module use the ~U sigil. |
Find more information about timezones and DateTime at https://hexdocs.pm/elixir/DateTime.html |
Recursions
Recursion is a fundamental concept in functional programming. If you’re familiar with loops in other programming languages (like for
or while
loops), recursion serves a similar purpose in Elixir. It allows you to perform a task repeatedly, but rather than using a loop, recursion involves a function calling itself.
A recursive function is a function that solves a problem by solving smaller instances of the same problem. To prevent infinite recursion, there must be one or more base cases where the function does not call itself.
Let’s break this down with a couple of simple examples.
Recursion Example: Countdown
Let’s imagine we want to create a countdown. Here’s a simple recursive function that achieves this:
iex> defmodule Example do
...> def countdown(1) do (1)
...> IO.puts "1" (2)
...> end
...>
...> def countdown(n) when is_integer(n) and n > 1 do (3)
...> IO.puts Integer.to_string(n) (4)
...> countdown(n - 1) (5)
...> end
...> end
iex> Example.countdown(4) (6)
4
3
2
1
:ok
1 | This is the base case: when countdown/1 is called with the argument 1 , this function matches. |
2 | We print 1 to STDOUT using IO.puts . |
3 | If countdown/1 is called with an integer greater than 1 (we don’t want negative input here), this function matches. |
4 | We convert the integer to a string using Integer.to_string(n) and print it. |
5 | The function calls itself, but with n decreased by 1 - this is the recursive step. |
6 | When we test the function, it correctly counts down from 4 to 1 . |
The countdown function keeps calling itself, each time reducing the initial number by one, until it reaches 1
, at which point it stops, thus preventing infinite recursion.
Recursion Example: Summing a List
Here’s another example where we calculate the sum of a list of integers using recursion:
iex> defmodule Example do
...> def sum([]) do (1)
...> 0
...> end
...>
...> def sum([head | tail]) do (2)
...> head + sum(tail) (3)
...> end
...> end
iex> Example.sum([10, 8, 12, 150]) (4)
180
1 | The base case: the sum of an empty list is 0 . |
2 | We pattern match a list and split it into a head (the first element) and a tail (the remaining elements). |
3 | We add the head to the result of the recursive call, which computes the sum of the tail . |
4 | The function correctly computes the sum of the list. |
Recursion Example: Transforming a List
You can use recursion to transform every element of a list. Let’s assume we want to double the value of every element of a list:
iex> defmodule Example do
...> def double([]) do (1)
...> []
...> end
...>
...> def double([head | tail]) do
...> [head * 2 | double(tail)] (2)
...> end
...> end
iex> Example.double([10, 5, 999])
[20, 10, 1998]
1 | Base case: An empty list results in an empty list. |
2 | We double the head and concatenate it with the result of the recursive call, which doubles the elements of the tail . |
Tackling Recursion
Unless you are doing this every day, you will get to problems where you know that recursion is a good solution, but you just can’t think of a good recursion for it. That is normal. Don’t worry.
I used to say that https://www.google.com and https://stackoverflow.com were your friends. They still are but ChatGPT and Github Copilot have made our lives as programmers so much easier. Ask them. No embarrassment!
During this book, we will work with recursions. So you’ll get a better feeling for it.
Exploring mix
mix
is a built-in tool in Elixir that helps to scaffold a new Elixir project with a pre-defined file and directory structure which makes getting started with a new Elixir application much easier. You don’t have to use it to create a new Elixir project but it is highly recommended and is used by most Elixir developers.
A simple "Hello, World!" application can be created with mix
as follows:
$ mix new hello_world
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/hello_world.ex
* creating test
* creating test/test_helper.exs
* creating test/hello_world_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd hello_world
mix test
Run "mix help" for more commands.
This command mix new hello_world
created a new directory named hello_world
and set up a basic Elixir application within it:
$ cd hello_world
$ tree
.
├── README.md
├── lib
│ └── hello_world.ex
├── mix.exs
└── test
├── hello_world_test.exs
└── test_helper.exs
3 directories, 5 files
The lib/hello_world.ex
file contains this code:
defmodule HelloWorld do
@moduledoc """
Documentation for `HelloWorld`.
"""
@doc """
Hello world.
## Examples
iex> HelloWorld.hello()
:world
"""
def hello do
:world
end
end
It only contains a single function, hello/0
, which returns the atom :world
.
This structure serves as a starting point for your application. The complexity of the structure may grow as your application grows or uses more sophisticated frameworks such as Phoenix.
iex -S mix
To start an iex
with the code of your current project, you can use iex -S mix
.
$ iex -S mix
Compiling 1 file (.ex)
Generated hello_world app
Erlang/OTP 26 [...]
Interactive Elixir (1.15.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
In this iex
you now have access to all the functions defined in your project:
iex(1)> HelloWorld.hello()
:world
iex(2)>
Code Formatting with mix format
mix format
is another powerful feature of mix
that automatically formats your Elixir source code files according to a set of standard conventions. This helps keep your code clean and consistent, and can save you and your team a lot of time in code reviews.
You can run mix format
in the root directory of your application:
$ mix format
This command will format all Elixir files in your project.
It’s a good habit to run mix format before committing any code to ensure that all code follows the same conventions. |
Testing with mix test
Elixir promotes a test-driven development (TDD) approach, and mix
makes it easy to create, manage, and run tests.
When you create a new project using mix
, a test
directory is created with a structure mirroring that of the lib
directory. This is where all of your test files will reside.
Let’s create a simple Elixir module and corresponding test.
lib/hello_world.ex
defmodule HelloWorld do
def greet(name) do
"Hello, #{name}!"
end
end
Now, let’s write a test for the greet
function.
test/hello_world_test.exs
defmodule HelloWorldTest do
use ExUnit.Case
doctest HelloWorld
test "greeting the world" do
assert HelloWorld.greet("world") == "Hello, world!"
end
end
The test
macro defines a test, while assert
checks that the actual result of the function matches the expected result.
You can now run the tests using mix test
:
$ mix test
....
Finished in 0.05 seconds
1 test, 0 failures
Randomized with seed 12345
In this example, mix test
ran 1 test, all of which passed.
Elixir also supports more complex testing scenarios, such as setup and teardown operations, test tagging, and asynchronous testing. As you write more complex Elixir programs, mix test
will become an indispensable part of your development workflow.
Custom mix
Tasks
mix
allows you to define custom tasks, making it a powerful tool for automating common development tasks. These custom tasks are Elixir scripts that can be run from the command line. For example, we can define a "Hello, world!" task.
Create a new directory lib/mix/tasks
and a new file within this directory named start.ex
:
lib/mix/tasks/start.ex
defmodule Mix.Tasks.Start do
use Mix.Task
def run(_) do (1)
IO.puts "Hello world!"
end
end
1 | The run(_) function is the entry point for our task. It gets called when we run the task. |
Now, running the command mix start
will print "Hello, world!" to the terminal:
$ mix start
Compiling 1 file (.ex)
Generated hello_world app
Hello world!
The .ex
file gets compiled and the start
task gets run. The compile step is only done when needed. If we call mix start
a second time, no compile is needed:
$ mix start
Hello world!
mix
is a vast topic, and we’ve only scratched the surface. But this should give you a basic understanding of how mix
can be utilized in an Elixir application.