Video:
Thank you for your interest in Elixir. And thank you for taking this course.
We’re both programmers, so I’ve tried to create a course as if we were both sitting at a computer and I was showing you a cool new language. I want this to be light and conversational, and not some big reference work. I’ll show you my mistakes (and I make a lot) to encourage you to try stuff and make your own.
The real way to learn a language is to use it, so we’ll be coding—a lot. We’ll be developing a hangman game. This may sound trivial, but it’s a surprisingly rich vein to tap.
The Plan
We’ll start by writing a dictionary module. This will let us select a random word for our game.
We’ll then write the logic of the game itself. We’ll put this into a separate application, which is the way I like to write Elixir. We’ll also learn how to manage dependencies, and how to split our code into small modules using the Single Responsibility Principle.
Next comes a simple text-based client. Here we’ll learn how to maintain application state in a functional language.
You’ve probably heard that Elixir is good at concurrent programming. We’ll see how this works when we look at processes and the actor model. We’ll start to convert our hangman game to use these ideas.
The elephant in the room is the OTP framework. This is a sophisticated way of managing large (and small) scale deployments of applications containing thousands of modules and millions of processes. We’ll use it to finish off the core of hangman.
Next comes the part you’ve all been waiting for—the Phoenix framework. We’ll add an HTTP front end to our application, and use it first to drive a conventional HTML client, and then to drive a single-page app with a JavaScript client.
How to Maximize The Experience
I’ll probably end up saying this too many times, but we learn by doing.
One thing I really want you to do is to code along as we build hangman. I’ll provide you with links to my code as we progress, just in case you need to reset. But please use these as a last resort. The more you code along, the faster you’ll learn.
You’ll come across blocks that look like this:
Your Turn
- Code a new module that lets the frobulator invert the flux during times of high cursplaining.
These are really important. Please, please do these exercises. Often they are designed to build on what you’ve seen, but then add some new twist. Working through them will teach you things.
The Most Important Rule
Explore, make mistakes, and try wild things.
And most of all, have fun.
Before we get started, we need to make sure we have the tools we need. The good news is that Elixir is packaged with everything we need—compiler, interactive shell, test framework, and a build tool.
Even better news is that the Elixir team takes packaging seriously, and have great installation options. Rather than take credit for their work, I’m going to refer you to their installation guide.
When you finish, bring up a command prompt on your system. Type1
$ elixir -v Erlang/OTP xx [erts-y.z] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-po ll:false] [dtrace] Elixir a.b.c
You, of course, will have your own prompt, probably highly customized and just perfect.
Right now, I’m using a development build of Elixir 1.5, but all the code here will run on Elixir 1.4.2 or later.
Install Hex
hex
is the Elixir package repository. It maintains a repository of Elixir and Erlang packages (at hex.pm. It also manages a local cache of these packages, and integrates with the mix
build tool to make them available to your apps. It’s also the tool you use to verify and publish packages for global consumption.
You install hex
using:
$ mix local.hex
It Says “Unknown command”
If your installation went OK, but your shell can’t find Elixir or mix, it’s likely an issue with your PATH. Tucked away in the installation instructions there may be notes on what you have to do to include Elixir.
Introduction to IEx
IEx is the single most useful tool for exploring and learning Elixir. I’ve been working with Elixir for years, but I still keep an IEx window open as I code.
This short video is a tour of some of the features of IEx that will be new to irb users.
Watch video at https://coursehunters.net/course/elixir-dlya-programmistov
Your Turn
- Use IEx to calculate
999999999 * 111111111
- Find the name of a function in the
String
module that removes leading and trailing whitespace from its parameter. - Use the
v
helper to divide the result calculated in step 1 by 9. - Try typing
h
(for help ) on its own.
Our First Project
Introduction
We’re going to explore some of Elixir’s tooling by building the first part of our Hangman game: the dictionary module.
Watch video at https://coursehunters.net/course/elixir-dlya-programmistov
Create a New Project
I’m going to be building the Hangman project in a series of units. We’ll start with a Dictionary application, then add the game logic later.
To get the most out of this course, I urge you to follow along, creating the project as I do. Doing so will let you do the various exercises I’ll throw at you. But, more importantly, it’ll let you start to build the muscle memory, or tacit knowledge, that is so vital to effective programming.
Anyway, let’s jump in.
Watch video at https://coursehunters.net/course/elixir-dlya-programmistov
Key Points
- create a new Elixir project using
$ mix new ⟪project_name⟫
- your code goes under
lib/
, and tests go undertest/
- the file
mix.exs
defines the configuration of your project - one way to compile the application is to use the command
$ mix
This must be used in the top-level directory of the application (the one containing the mix.exs
file)
Your Turn
- Try entering
mix help
in your terminal. - Then try
mix help run
, - and
iex -h
, - and finally
elixir -h
There’s a lot of information here, but you only need to know a few options and commands to write your apps. Still, it’s good to know there’s help for when you need it.
Run Some Code
Now we have our project structure in place, it’s time to start coding. As our source code lives under lib/
, and our project is called dictionary
, mix already created a file called lib/dictionary.ex
, just waiting for us to start hacking.
Watch video at https://coursehunters.net/course/elixir-dlya-programmistov
Things we saw
Language Stuff
- You can group a chunk of Elixir code between the keywords
do
andend
. This is used in our example to delineate the body of our module, and the bodies of the two functions it contains:
efmodule Dictionary do
# body of module . . .
end
def random_word() do
# body of function . . .
end
- modules in Elixir are defined using
defmodule
followed by the module name and ado
/end
block. - functions are defined using
def
followed by the function name, any parameters, and the body in ado
/end
block. - module names must be an Elixir atom . Conventionally we use Capitalized words (
MyFirstModule
). There’s a section about atoms later in this chapter. - Function names are either names or one of the Elixir operators. Names must start with a lowercase letter or underscore, and may contain letters, digits, and underscores. The name may end with an exclamation point or a question mark.
-
IO.puts
writes a string to standard output.
Tool stuff
-
mix
on its own compiles your project -
mix run
runs it, andmix run -e ⟪code⟫
executes the code in the context of your project -
iex -S mix
startsiex
in the context of your project—it usesmix
to build the application environment and then entersiex
- inside
iex
iex> r ModuleName # recompiles the file containing ModuleName
iex> c "lib/name.ex" # compiles the given file
Your Turn
If you haven’t already coded along with the video, do it now.
Run the hello function you wrote using both mix and iex.
Keep iex running, and change the message you output in the program source. Save it away. Reload the file in iex using the r command and rerun your function.
Introduce some syntax errors into your module. Leave off a do or an end, misspell def and so on. Have a look at the error messages when you compile, and see how they relate to the changes you made.
Write the Dictionary Module
This is a longer video, because we’re going to do all the work to implement our dictionary module. We’ll also talk a little about refactoring code into a more functional style.
Watch video at https://coursehunters.net/course/elixir-dlya-programmistov
Here’s the link to download the word list file.
What We Did
Create an assets directory in our project and download words.txt into it.
Went into iex, and typed File.↦ (where ↦ represents the tab character). Tab completion displayed the list of File functions, and we use h File.read to get help.
We chose File.read! to read the wordlist and String.split to break it into a list of words.
We combined the two into a function, Dictionary.word_list, and played with it in iex.
We also saw examples of tuples and regular expression literals. We’ll look at these in more detail in the next chapter.
Remember
Strings Can Be Called Binaries
Erlang uses sequences of bytes to represent strings. It considers this just another byte stream—binary data—and so calls these values binaries. This convention carries forward into Elixir, so don’t be surprised to see the word binary where you were expecting string.
Function Names Ending With An Exclamation Mark
Often Elixir has two variants of a function, such as File.read and File.read!. Most of the time this means that the plain version will return an error status on failure, and the version with the shriek will raise an exception. This rule isn’t universal, though, so check the documentation.
Functions Are Identified By Name/Arity
Elixir functions have a name (by convention lowercase letters, digits, and underscores, with a possible trailing ? or !). They also have an arity: the number of parameters they take.
Both the name and the arity are required to identify a particular function. We write the combination as name/arity: String.split/1, String.split/2 and so on.
Values Are Not Objects
In an object-oriented language, an object is typically associated with a class or a prototype. That class defines the methods that are supported by the object. In Ruby, for example, we could split a string using:
"123\n456\n789\n".split(/\n/)
Values in Elixir have a type, but the type does not attach functions to those values. Instead, you pass the value as a parameter to the appropriate function:
String.split("123\n456\n789\n", ~r/\n/)
When I first started using Elixir, I hated this. It seemed as if I had lost the fact that (say) strings had string-like behaviors.
But the more I coded in Elixir, the more I came to realize that this association between a value and a set of methods actually represented a very strong form of coupling, and that this coupling had made my OO code brittle.
It takes getting used to, but I think you’ll come to the same conclusion.
Finding Functions
Although types don’t have associated functions, you’ll find that the libraries in Elixir use naming conventions to make finding appropriate functions easier. For example, a string will typically be manipulated by functions in the String module (String.split and so on). Similarly, lists are manipulated by functions in the List module.
Sometimes a set of functions applies to values of multiple types. For example, lists and maps are both collections of values—they can be enumerated. Functions to do with this shared behavior are in their own module (in this case Enum).
Functions Return The Last Expression Evaluated
Functions exit by falling out the bottom—there is no explicit return keyword.
Your Turn
This quick exercise will get you familiar with finding functions. Try the following in IEx:
Bind the string “had we but world enough, and time” to a variable.
use functions from the String module to
Split it into two parts: the stuff before and the stuff after the comma.
Split it into a list of characters, where each entry in the list is a single character string.
Split it into a list of characters, where each entry in the list is the integer representation of that character.
reverse the string (hmmm… the first two words of the result are interesting)
calculate the set of differences between this string and `“had we but bacon enough, and treacle”; you should get
[
eq: "had we but ", del: "w", ins: "bac", eq: "o",
del: "rld", ins: "n", eq: " enough, and t", del: "im",
ins: "r", eq: "e", ins: "acle"
]
Refactor Into Pipelines
Watch video at https://coursehunters.net/course/elixir-dlya-programmistov
OO Has Methods, FP Has Functions
In object-oriented programming, we tell objects to change their state by invoking instance methods. This is convenient, but if you’ve been doing OO programming for a while, you’ll also know it has some drawbacks.
First, there’s the coupling between state and behavior. In an ideal world, this wouldn’t be a problem. But in the real world, we don’t really have tidy encapsulation like this. Instead, we find ourselves constantly subclassing just so we can share methods. If you’re a Rails developer, this is your dominant model of programming.
Second, there’s confusion about rôles. The state stored in a class is tied to the rôle implemented by that class. If you need to have it participate in other ways, you have to extend the class, perhaps with mixins or (heaven forbid!) subclassing. Yet more coupling.
Third (and in today’s world probably the most significant issue) is the idea that methods mutate object state. In a concurrent system, if you have a reference to an object, you have no guarantees that the value of that object won’t just change, even if you do nothing to it. We fix that with various synchronization techniques; it is difficult (read next to impossible) to verify that we did this correctly.
Functions and State
In the world of functional programming, state is decoupled from behavior. State is always immutable—it represents a fact that is true at some point in the life our your code.
Functions transform state into new state. They never change the state that’s given to them. This means that ideal functions are pure: given a particular input, they will always produce the same output. In turn, this means that functions are easy to compose and reuse.
Our goal when using a functional paradigm is to think out our program as one big function, transforming its inputs into outputs. We then break this down into progressively smaller functions, until we end up with a bunch of small functions which each does just one thing.
Our main tools are functional composition and pattern matching.
Functional Composition
Composition means chaining together functions so that the output of one becomes the input of the next. In our dictionary code, we used pipelines to compose the File.read! and String.split functions. Once joined like this, they act as if they are a single function: take a file name and return a list of words.
Pattern Matching
Pattern matching lets you write different versions of the same function. The version that is called depends on the value that is passed in. This is similar to the idea of overloaded methods in some OO languages. For example, here’s an inefficient implementation of a Fibonacci calculator written using pattern matching:
def fib(0), do: 0
def fib(1), do: 1
def fib(n), do: fib(n-1) + fib(n-2)
We have a whole chapter on pattern matching coming up.
The Pipeline Operator, |>
The |> operator is how we compose functions in Elixir. It takes the result of the expression on its left and injects it as the first parameter of the function call to its right.
This means that
"1-2-3" |> String.split("-")
is the same as writing
String.split("1-2-3", "-")
Pipelines get more interesting when there are multiple steps. For example, to see how many values there are after the split, we could write:
values = String.split("1-2-3", "-")
IO.puts length(values)
or
IO.puts length(String.split("1-2-3", "-"))
Try explaining to a nonprogrammer how to work out what gets done first in that code.
But, using pipelines, we can write:
"1-2-3"
|> String.split("-")
|> length
|> puts
This is an elegant and clear set of transformations from a string, to a list, to an integer.
When you’re just starting out with Elixir, try to make yourself use pipelines all the time. A good way to remind yourself is to try not to use local variables. You won’t always succeed—sometimes you just need to use them. But thinking about eliminating them will help you think in terms of transformations and pipelines
Onward!
We’ve done a lot in a single chapter. We’ve written a functional Elixir module, using pipelines to transform data. We’ve explored Elixir tooling, and seen how to apply functions to values.
We’ve done this without diving into too many details. In the next chapter we’ll rectify this, looking at the types baked into Elixir.
If you haven’t already done so, I strongly suggest that you implement the Dictionary application for yourself. Looking at videos is all very well, but there’s nothing like a little muscle memory to bake the ideas into your head.
My version is also available in Github:
$ git clone https://github.com/pragdave/e4p-code.git
$ git checkout 020-initial-dictionary-complete
$ cd game/dictionary
A Mad Dash Through Elixir Types
Introduction
Let’s look at the types that come with Elixir. In this chapter we’ll cover
integers and floats
atoms
booleans
ranges
strings
regular expressions
tuples
lists
maps
No one likes to plow through a bunch of obvious facts, and you’re all programmers already, so in these two chapters I try to be brief and just point out the extra things I think you need to know.
Integers and Floats
Elixir integers can be arbitrarily big (within the limits of available memory).
You can write them with underscores between the digits. Normally this is used as a surrogate for commas.
The prefixes 0x, 0o, and 0b indicate literals using hexadecimal, octal, and binary notation. (Note that you cannot use just a leading zero to indicate an octal constant.)
Finally, ?c is the codepoint of the UTF character c.
iex> 123_456_789 123456789 iex> 0x41 65 iex> 0o101 65 iex> 0b100_0001 65 iex> ?A 65 iex> ?≠ 8800
Useful Functions
Integer division always returns a float. The function div returns a truncated integer result. trunc and round return a truncated and rounded integer given a float.
iex> 8/3
2.6666666666666665
iex> div(8, 3)
2
iex> trunc(8/3)
2
iex> round(8/3)
3
String.to_integer does the obvious:
iex(18)> String.to_integer("65")
65
iex(19)> String.to_integer("41", 16)
65
iex(20)> String.to_integer("1000001", 2)
65
Floats
Floats are stored in IEEE 754 format. Literal floats must have at least one digit before and after the decimal point, and may have an e suffix to indicate an exponent.
Atoms
Atoms are constants that are used to name things. In a way they are just like strings—the difference is in the representation.
There are two ways of specifying an atom in Elixir. The first is to prefix a name or an operator with a colon. Here are some atoms using that notation:
:cat :puppy_dog :>=
Sometimes you need to create atoms that contain characters that aren’t allowed in normal names. Do this by enclosing them in double quotes:
:"cat-dog" :"now is the time" :"!@$%^&UIO"
This format also allows you to embed the result of evaluating code in your atom names:
iex> a = 99
99
iex> :"next-number: #{a+1}"
:"next-number: 100"
The second way to create an atom is to use a name that starts with an uppercase letter. This is what our module names are. When we write defmodule Dictionary, Elixir converts Dictionary into an atom. However, the Erlang standard libraries, which Elixir uses, have hundreds of atoms already defined for their modules. To avoid a name clash, Elixir automatically adds the prefix Elixir. to atoms derived from capitalized names. This means that
iex> is_atom(Dictionary)
true
iex> Dictionary == :"Elixir.Dictionary"
true
Booleans
Elixir uses the constants true and false for boolean values. It also has the constant nil.
However, in most contexts that expect a boolean expression, Elixir is more lax. The values nil and false are considered falsy; all other values are truthy.
There are two sets of logical operators. The ones using traditional operator symbols (&&, ||, !) work with truthy values. Operators that are spelled-out words (and, or, not) only accept true or false on their left hand side.
&& and and return their right hand value if the left hand is truthy (or true), and || and or return the right hand value if the left is falsy.
iex> fname = "/etc/passwd"
"/etc/passwd"
iex> content = File.exists?(fname) && File.read!(fname)
"User Database\n\n Note that this file is consulted
. . . . .
iex> fname = "/etc/not-a-file"
"/etc/not-a-file"
iex(18)> content = File.exists?(fname) && File.read!(fname)
false
Can I use the logical operators to do bit operations?
In short, no.
But don’t despair! If your code uses the library Bitwise you have access to a slew of bit-oriented operators.
iex> h Bitwise.
&&&/2 <<</2 >>>/2 ^^^/2 band/2 bnot/1
bor/2 bsl/2 bsr/2 bxor/2 |||/2 ~~~/1
iex> use Bitwise
Bitwise
iex> 1 <<< 10
1024
iex> 0b11111010 &&& 0b10101111
170
iex> 3 ||| 6
7
Ranges
Ranges represent a bounded set of integers. They are typically used in two contexts.
First, they are used with the in operator to check that an integer falls between two bounds:
iex> a = 5..10
5..10
iex> b = 8..3
8..3
iex> 4 in a
false
iex> 4 in b
true
Second, they are enumerable, and so are used as the starting point for different kinds of iteration (which we cover later).
iex> for i <- b, do: i*3
[24, 21, 18, 15, 12, 9]
Strings (and Sigils)
In our Dictionary, we split the word file into words using String.split. We wrote it as:
String.split(str, ~r/\n/)
The syntax ~r… is an example of an Elixir sigil—a generic way of writing string-like constants.
A sigil is simply a notation for creating values from strings. In the regular expression example, the string was \n and the value created was the corresponding regular expression.
All sigils start with the tilde character, ~. This is followed by a single letter, which determines the type of value to be generated. Next comes the string, between delimiters, and finally there may be some optional flags.
The delimiters can be:
~r/.../ ~r"..." -r'...' ~r|...|
~r<...> ~r[...] ~r(...) ~r{...}
~r"""
:
:
"""
Note that in the first line the same character is used as the opening and closing delimiter, while in the second line we use matching pairs of characters. The third row shows the multiline form of string constants.
Finally, the optional flags are simply a string of letters. Their interpretation depends on the sigil type. For example, ~r/cat/i has the flag i, which makes the pattern match case insensitive.
The sigils that come as part of Elixir are:
~c// ~C// list of character codes
~r// ~R// regular expression
~s// ~S// string
~w// ~W// list of words
Here are some examples:
iex> ~c/cat\0/
[99, 97, 116, 0]
iex> ~r/cat/i
~r/cat/i
iex> ~s/dog/
"dog"
iex> ~w/now is the time/
["now", "is", "the", "time"]
The lowercase versions of sigils expand escape sequences and interpolate embedded expressions:
iex> name = "Betty"
"Betty"
iex> ~s/Hello #{name}\n/
"Hello Betty\n"
iex> ~c/#{name}\0/
[66, 101, 116, 116, 121, 0]
The uppercase equivalents do no expansion:
iex> ~S/Hello #{name}\n/
"Hello \#{name}\\n"
There are three additional builtin sigils, ~D//, ~N//, and ~T//. These generate dates and times.
Finally, you can add your own sigils to the language by writing appropriately named functions. This is rarely done.
Strings
An Elixir string is a sequence of Unicode codepoints. They look and behave much like strings in other languages, although they are immutable.
You can write a string as “hello”. This is equivalent to ~s{hello}. Be careful not to use single quotes when you want a string. ‘hello’ is the same as ~c{hello}; it generates a list of character codepoints.
Backslash expansion and expression interpolation is enabled in double-quoted strings:
"Name:\t#{name}\nAge:\t#{ trunc(age) }"
To turn off these substitutions, use the sigil form with an upper case S as the type:
~S"Name:\t#{name}\nAge:\t#{ trunc(age) }"
The operator <> concatenates strings, and the functions in the String module manipulate them.
Your Turn
Back into IEx you go…
The function Time.utc_now returns the UTC time. Interpolate it into a string using both a double-quote literal and a sigil.
When does the interpolated expression get evaluated?
You have to output the instructions on how to interpolate expressions into strings. Use IO.puts to output:
For example, 1 + 2 = #{ 1 + 2 }
Use a sigil to transform “now is the time” into the list
[ "now", "is", "the", "time" ]
Regular Expressions
As we’ve already seen, an Elixir regular expression literal is written:
~r/name:\s*(\w+)/
The underlying regular expression engine is PCRE.
The Regex module contains most of the functions that work with regular expressions. In addition the operator =~ can perform a regular expression match.
iex> str = "once upon a time"
"once upon a time"
iex> str =~ ~r/u..n/
true
iex> str =~ ~r/u..m/
false
(A little known fact: =~ also accepts a string as its right hand argument, it which case it returns true if the left string contains the right.)
Your Turn
Time to explore the functions in the Regex module:
Write an expression that returns true if a string contains an a, followed by any character, then a c (so abc, and arc will return true, and ace will not).
Write an expression that takes a string and replaces every occurrence of cat with dog.
Do the same, but only replace the first occurrence.
Tuples
A tuple is a fix-length collection of values. Back when you did geometry in school, you used tuples all the time: {x,y} and {x,y,z} are both tuples.
Tuple literals are written as a list of expressions between braces:
{ :ok, "wilma" }
{ :reply, destination, "rain with chance of hail" }
{ 1, 2, 3+4 }
Typically tuples are small (two or three elements). They are frequently used to pass flagged values to and from functions. For example, if successful, File.read returns the tuple:
{ :ok, contents }
The first element is the atom :ok, and the second is the contents of the file.
If instead the file could not be read, File.read would return:
{ :error, reason }
where reason is an explanation of the failure.
This might seem a little clunky, but later on we’ll look at pattern matching and we’ll see that tuples are actually easy to manipulate.
Lists
Lists are a central part of Elixir. This isn’t just because they are a useful data structure. It’s also because the definition and structure of lists is ideally suited to a functional language.
In this unit we’ll see what a list is, how you write them, and the techniques for processing them.
Before we start, here’s a subtle message:
Lists
are not
Arrays!
An array is a contiguous area of memory containing fixed size cells.
If you want to find the nth element in a array, you perform some simple address arithmetic:
(this, by the way, is why programmers count from zero, not one)
Lists
Lists are a sequence of zero of more elements, one linked to the next.
To get to the third element of the list, you have to start at the first and follow the links.
However, lists have an extremely useful property. It is easy to add a new element at the head of a list, and equally easy to remove that first element.
This property is reflected in the definition of a list:
A list is either:
the empty list, written [ ], or
a value followed by a list, written [ value | list ]
The value part of the definition is normally called the head of the list, and the rest is called the tail.
Here’s how we can build up a list, element by element:
empty = []
list_3 = [ 3 | empty ]
list_2_3 = [ 2 | list_3 ]
list_1_2_3 = [ 1 | list_2_3 ]
Your Turn
Go ahead and enter these lines into IEx.
Did you notice something interesting?
iex> empty = []
[]
iex> list_3 = [ 3 | empty ]
[3]
iex> list_2_3 = [ 2 | list_3 ]
[2, 3]
iex> list_1_2_3 = [ 1 | list_2_3 ]
[1, 2, 3]
Even though you built the lists as a series of head/tail pairs, IEx displayed the result as a simple list of values.
That’s a convention in Elixir. Rather than making you build lists the hard way, you can write them using the conventional comma-separated list of values. But never forget that nonempty lists always have a head, which is a value, and a tail, which is a list.
Lists vs. Arrays
The chances are very good that your current language has arrays. They’re probably one of your go-to data structures. You like being able to say “go get the nth element.”
But arrays are a data structure suited more to imperative programming, because you must explicitly iterate over them, using an external index. And they are prone to off-by-one errors.
On the other hand, lists are a recursive data structure, and turn out to be well suited to a functional or declarative style.
It’s a change of perspective that can be hard to make. Stick at it, because once you’re comfortable with lists, you won’t want to go back.
To show you some of the power of lists, we need to cover just one more language topic: pattern matching. That’s what the next chapter is about.
Improper Lists
There’s More. Lots More
We’re going to dig into lists in more detail when we look at pattern matching. For now, we have just one more type to look at, the Map.
Maps
Some languages call them dictionaries, hashes, or associative arrays. Elixir calls them maps.
iex> countries = %{
...> "BFA" => "Burkina Faso",
...> "BDI" => "Burundi",
...> "KHM" => "Cambodia",
...> "CMR" => "Cameroon",
...> "CAN" => "Canada",
...> }
%{"BDI" => "Burundi", "BFA" => "Burkina Faso",
"CAN" => "Canada", "CMR" => "Cameroon",
"KHM" => "Cambodia"}
iex> countries["BFA"]
"Burkina Faso"
iex> countries["XXX"]
nil
iex> countries[123]
nil
Maps are an unordered collection of key/value pairs. Both keys and values can be any Elixir type, and those types can be mixed within a map.
As the example shows, map literals look like
%{ key1 => value1, key2 => value2, . . . }
You use the functions in the Map and Enum modules to work with maps. In addition, Elixir provides the map[key] shortcut to return the value corresponding to key (or nil).
When the Keys are Atoms
We often use maps as lookup tables, where the keys are all atoms. For those cases, Elixir has a shortcut syntax.
iex> multipliers = %{ once: 1, twice: 2, thrice: 3 }
%{once: 1, thrice: 3, twice: 2}
iex> 5 * multipliers[:twice]
10
iex> 5 * multipliers.twice
10
In a map constant, once: 1 is the same as writing :once => 1. And if the map’s keys are atoms, you can access the values using the map.key notation.
Your Turn
There is a difference between map.key and map[:key]. Can you play in IEx and find it?
I Probably Shouldn’t Tell You This, But…
Remember back when we were talking about lists, I said that lists are not arrays, and that could couldn’t index into them in constant time?
Well, if you really, really need array-like behavior, you could always use a map with integer keys:
iex> squares = %{ 0 => 0, 1 => 1, 2 => 4, 3 => 9 }
%{0 => 0, 1 => 1, 2 => 4, 3 => 9}
iex> squares[2]
4
The performance will be perhaps an order of magnitude slower than a pure array (if Elixir had pure arrays), but the lookup characteristics will be near O(1) for reasonably sized collections. And, as an added benefit, you get sparse arrays for free.
Having said that, I can only remember using this technique once in the last few years.