
To print this out we can use the IO.inspect
function for pretty printing:
IO.inspect(book_lengths)
[17, 12, 9, 9, 21]
Elixir’s collection types
We’ve already seen the List
type in action. Elixir includes these main collection types:
- List: An immutable, but designed for modification-by-duplication, homogenous collection of arbitrary types.
- Syntax: Square brackets with items:
[x,y,z]
- Syntax: Square brackets with items:
- Tuple: Designed for holding values primarily, not manipulation, tuples are like lists but geared towards read performance. Think of them a data access kind of collection.
- Syntax: Curly braces with items:
{x,y,z}
- Syntax: Curly braces with items:
- Keywords List: Ordered key-value pairs, string-only keys, mainly used for named arguments for functions
- Syntax: Square brackets with pairs:
{x: x1,y: y1,z: z1}
- Syntax: Square brackets with pairs:
- Maps: The familiar key-value pairs, where keys can be anything and the collection is unordered.
- Syntax: Percent curly braces with pairs:
%{x => x1, y => y1, z => z1}
%{x: x1, y: y1, z: z1}
- Syntax: Percent curly braces with pairs:
Maps and atoms
Maps have two styles of declaration, and the one to use depends on whether the keys are atoms. An atom is a variable whose value is the same as its name, a kind of super-constant. An atom is declared with a colon followed by a literal.
We could create a map of string keys to integer values like so:
books_and_lengths = %{ "The Bhagavad Gita" => 17, "Tao Te Ching" => 12 }
The following is different, and creates a map of atoms to integers, probably not what we want in this case:
books_and_lengths = %{ "The Bhagavad Gita": 17, "Tao Te Ching": 12 }
Note the placement of the colon. In a Map
, the colon being directly next to the kay indicates it’s an atom, and atoms can be quote-enclosed (to support otherwise illegal characters).
The bottom line is to use the arrow syntax (=>
) when you want a normal variable and the key and the colon (:
) when you want atoms.
Normally, atoms are declared like this:
:my_atom
Here’s another way to declare a map with atom keys:
my_map = %{:atom1 => “foo”, :atom2 => “bar”}
Modules
Elixir supports modules, which are namespaces that gather together related functions. It does not hold state or variables like a class or code block. As you’d expect, you can call other functions from within the same module, but those calling in from outside need to preface the calls or import the module.
Here’s a simple module:
defmodule BookFunctions do
def myFunc
end
end
BookFunctions.myFunc()
Pattern matching
Syntactic flavors and standard library characteristics go a long way to making up the overall feeling of using a language. They are the commonplace features you interact with all the time. But every language has some features that stand out.
Functional pattern matching is a sophisticated and lovely feature that Elixir brings to the table, allowing you to perform conditional function execution in a switch-like syntax. Let’s say we want to output small, medium, or long based on the book title lengths:
defmodule BookFunctions do
def categorize_length(length) do
case length do
length when length <= 10 -> "Short"
length when length <= 20 -> "Medium"
_ -> "Long"
end
end
def print_categories(lengths) do
Enum.each(lengths, fn length ->
category = categorize_length(length)
IO.puts("#{length} characters: #{category}")
end)
end
end
A couple of notes:
BookFunctions
is a module, which you’ve seen.
- In Elixir, return statements are implied, so the
categorize_length()
function automatically returns whatever is the result of the last expression.
The case
keyword is what creates the pattern-matching block, in the categorize_length
function. The length when length
syntax (technically, a guard clause) lets us do range checking on the length variable, and if it meets the criteria, the ->
operator tells us what to return from the case. (Since this is the final statement of the function, it will also be the functional return value.)
We could use this new function on our book_lengths
like so:
BookBookFunctions.print_categories(book_lengths)
17 characters: Medium
12 characters: Medium
9 characters: Short
9 characters: Short
21 characters: Long
Enum.each
is analogous to forEach
in other languages like JavaScript, letting us perform an operation on each element of a collection.
Looping
Elixir does not have for and while loops. This can be a bit shocking at first, but it is in line with the immutability favored by functional philosophy. In essence, Elixir doesn’t want you doing mutation during loops, and instead wants you to use recursion. Recursion keeps you in the realm of functions, and ideally, you want to use pure functions (meaning, functions without side-effects).
Much of the looping you need to do can be handled with functional operations like Enum.each
and Enum.map
. The Elixir forum has a good, extensive discussion of looping and alternatives.
Comprehensions
One of the most direct ways to simulate a for loop is with comprehensions, which you would be forgiven for mistaking for an actual for loop:
for x <- 0..10, do: IO.puts x
See the Elixir docs for more on comprehensions and how they simplify collection operations.
Pipe operator
The pipe operator gives you a clean syntax for chaining function results together. Think of it as a more elegant form of nesting functions. Here’s a simple example of the pipe operator on our books_and_lengths
collection:
books_and_lengths
|> Map.keys()
|> Enum.map(&String.upcase/1)
|> Enum.join(", ")
|> IO.puts()
The output is:
The Bhagavad Gita, Tao Te Ching
Concurrency
Although concurrency is a complex topic, it’s one of Elixir’s areas of strength, so let’s take a quick look. Elixir uses Actors, something like virtual threads in that they are not full operating-system processes. Actors support message-passing for simplified concurrent communication.
The following example, demonstrating message handling, is from the Elixir docs:
defmodule Example do
def listen do
receive do
{:ok, "hello"} -> IO.puts("World")
end
listen()
end
end
Notice that the listen
function is recursive (it calls itself at the end), which lets it handle multiple messages. Without the recursion the process would exit.
To launch this Actor, we use spawn
:
pid = spawn(Example, :listen, [])
Then we can send a message from the main process, using the pid
we saved:
send pid, {:ok, "hello"}
This outputs “World” to the console.
Conclusion
Languages are defined largely by what they make easy and what they make hard. Elixir is clearly devoted to making it easy for a programmer to stay in the functional programming mentality, and harder to stray into mutations and side-effects.
The overall effect is that you tend to write good functional programming code, as long as you work with the language and don’t fight it. It’s not hard to see why Elixir has captured so much interest, bringing Erlang’s legacy into the modern world. It’s a programming language with strong ideas about how to do things and an active, enthusiastic community.