Modules are the main building blocks of Elixir programs. Modules can contain named functions, import functions from other modules, and use macros for powerful composition techniques.
Open up your favorite editor and create your first Elixir program:
defmodule Rocket do
def start_launch_sequence do
seconds = 10
IO.puts "T - #{seconds}..."
countdown(seconds)
end
defp countdown(seconds) do
for i <- seconds - 1 .. 1 do
IO.puts "#{i}..."
end
blastoff()
end
defp blastoff do
IO.puts "Liftoff!"
end
end
Rocket.start_launch_sequence()We can compile and run our program in a single step using the elixir command:
$ elixir examples/rocket.exs
T - 10...
9...
8...
7...
6...
5...
4...
3...
2...
1...
Liftoff!Let's analyze this example bit by bit.
Elixir files are named
.exs, which stands for Elixir script, and.exare typically compiled to Erlang BEAM byte-code. For simple one off programs, sys-admin scripts etc. use.exs.
You can also fire up iex in the same directory and use the c helper function
iex> c "examples/rocket.exs"
T - 10...
...Publicly reachable functions are defined with the def keyword, while private functions use defp. It is common with Elixir code to group public functions with their private counterparts instead of lumping all public and private functions as separate groups in the source file. Attempting to call a defp function will result in an error:
iex> Rocket.countdown(1)
** (UndefinedFunctionError) undefined function: Rocket.countdown/1
Rocket.countdown(1)A few keywords exist in Elixir that live at the heart of module composition:
aliasused to register aliases for modulesimportimports functions and macros from other modulesrequireensures modules are compiled and loaded so that macros can be invoked
In the example below :math refers to the Erlang math module and makes
it accessible as Math (following the Elixir naming convention for
Modules).
Furthermore, we could have imported the entire Math module with import Math; however, since we only wish to call the pi function, we've limited the
import to only that specific function.
defmodule Converter do
alias :math, as: Math
import Math, only: [pi: 0]
def degrees_to_radians(degrees) do
degrees * (pi / 180)
end
def sin_to_cos(x) do
Math.cos(x - (pi/2))
end
end
{:module, Converter...
iex> Converter.degrees_to_radians(90)
1.5707963267948966
iex> Converter.sin_to_cos(120)
0.5806111842123187Rather than calling cos via the Math module, we could have imported it
as well
import Math, only: [pi: 0, cos: 1]
def sin_to_cos(x) do
cos(x - (pi/2))
endThe easiest way to get started is firing up iex, or Interactive Elixir, to experiment with code live in the Elixir shell. The code examples in this chapter were entered directly in iex and you are encouraged to follow along in your own session.
- term - An element of any data type and is commonly referred to as such in documentation and code examples
- literal - A value within source code representing a type
- bitstring - Used to store an area of untyped memory
- binary - Bitstrings containing a number of bits evenly divisible by eight and commonly used to store UTF-8 encoded strings
- arity - The number of arguments a function accepts
iex> result = if 1 == 1 do
...(1)> "correct"
...(1)> else
...(1)> "incorrect"
...(1)> end
"correct"
iex> result
"correct"
Variables are immutable. Once assigned, they cannot be changed. Instead of operating on information hiding and mutating shared state, Elixir programs are constructed around data transformation and message passing among isolated processes. This follows the Actor Model of concurrency.
Parenthesis are optional as long as their absence does not introduce ambiguity.
Documentation in Elixir is written in Markdown and compiled into the program as metadata. This allows formatted documentation to be brought up on-demand through iex.
Additional thing that is very convenient is doctest. You can write tests inside your comments above function definition (similar feature is available in Python).
Example for such is here and here are tests.
Elixir programs compile to the Erlang Abstract Format, or byte-code for Erlang's BEAM Virtual Machine. Erlang modules can be called from Elixir and vice-versa. Calling Erlang modules from Elixir simply requires using an atom by prefixing the module name with a semicolon.
iex> IO.puts "Print from Elixir"
Print from Elixir
:ok
iex> :io.fwrite "Print from Erlang~n"
Print from Erlang
:ok
iex> :math.pi
3.141592653589793
iex> :erlang.time
{19, 41, 20}Elixir is dynamically typed and contains a small, but powerful set of types including:
- Integer
- Float
- Atom
- Tuple
- List
- Bitstring
- Pid
Atoms are constants with a name and synonymous with symbols in languages such as Ruby. Atoms are prefixed by a semicolon, such as :ok and are a fundamental utility in Elixir. Atoms are used for powerful pattern matching techniques, as well as a simple, yet effective way to describe data and return values. Internally, Elixir programs are represented by an AST (Abstract Syntax Tree) comprised of atoms and metadata.
iex> is_atom :ok
trueTuples are arrays of fixed length, stored contiguously in memory, which can hold any combination of Elixir types. Unlike Erlang, tuples in Elixir are indexed starting at zero.
iex> ids = {1, 2, 3}
{1, 2, 3}
iex> is_tuple ids
true
iex> elem ids, 0
1
iex> elem ids, 1
2Lists are linked-lists containing a variable number of terms. Like tuples, lists can hold any combination of types. Element lookup is O(N), but like most functional languages, composing lists as a head and tail is highly optimized. The head of the list is the first element, with the tail containing the remaining set. This syntax is denoted by [h|t] and can be used to show a list entirely as a series of linked lists. For example:
iex> list = [1, 2 ,3]
[1, 2, 3]
iex> [ 1 | [2, 3] ] == list
true
iex> [1 | [2 | [3]] ] == list
true
iex> hd list
1
iex> tl list
[2, 3]
iex> [head|tail] = list
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]
iex> h Enum.at
def at(collection, n, default // nil)
Finds the element at the given index (zero-based).
Returns default if index is out of bounds.
Examples
┃ iex> Enum.at([2, 4, 6], 0)
┃ 2
┃ iex> Enum.at([2, 4, 6], 2)
┃ 6
┃ iex> Enum.at([2, 4, 6], 4)
┃ nil
┃ iex> Enum.at([2, 4, 6], 4, :none)
┃ :none
iex> Enum.at list, 2
3
iex> Enum.reverse list
[3, 2, 1]Use
hfollowed by a Module name or Module function name to call up markdown formatted documentation as seen in the ninth iex entry of the previous example.
Keyword Lists provide syntactic sugar for using a list to represent a series of key-value pairs. Internally, the key-value pairs are simply a list of tuples containing two terms, an atom and value. Keyword lists are convenient for small sets of data where true hash or map based lookup performance is not a concern.
iex> types = [atom: "Atom", tuple: "Tuple"]
[atom: "Atom", tuple: "Tuple"]
iex> types[:atom]
"Atom"
iex> types[:not_exists]
nil
iex> types == [{:atom, "Atom"}, {:tuple, "Tuple"}]
true
iex> IO.inspect types
[atom: "Atom", tuple: "Tuple"]
iex> IO.inspect types, raw: true
[{:atom, "Atom"}, {:tuple, "Tuple"}]
iex> Keyword.keys(types)
[:atom, :tuple]
iex> Keyword.values types
["Atom", "Tuple"]
iex> Keyword.
delete/2 delete_first/2 drop/2 equal?/2
fetch!/2 fetch/2 from_enum/1 get/3
get_values/2 has_key?/2 keys/1 keyword?/1
merge/2 merge/3 new/0 new/1
new/2 pop/3 pop_first/3 put/3
put_new/3 split/2 take/2 update!/3
update/4 values/1Gratuitous use helps discover new functions and explore module APIs
Maps are key/value stores and synonymous with hashes or dictionaries in other languages. Maps support powerful pattern matching and upsert operations and are defined with the %{} syntax.
iex> map = %{name: "elixir", age: 3, parent: "erlang"}
%{age: 3, name: "elixir", parent: "erlang"}
iex> map_with_strings = %{"name" => "elixir", "age" => 3, "parent" => "erlang"}
%{"age" => 3, "name" => "elixir", "parent" => "erlang"}
iex> map_with_strings["name"]
"elixir"
iex> map_with_strings[:name]
nil
iex> %{name: name} = map
%{age: 3, name: "elixir", parent: "erlang"}
iex> %{age: age} = map
%{age: 3, name: "elixir", parent: "erlang"}
iex> age
3
iex> map = %{map | age: 4}
%{age: 4, name: "elixir", parent: "erlang"}
iex> map = %{map | age: 4, new_key: "new val"}
** (ArgumentError) argument error
(stdlib) :maps.update(:new_key, "new val", %{age: 4, name: "elixir", parent: "erlang"})Structs are tagged maps used for polymorphic dispatch and pattern matching.
iex>
defmodule Tweet do
defstruct id: nil, text: "", username: nil, hash_tags: [], mentions: []
end
iex> status = %Tweet{text: "Chirp!"}
%Tweet{id: nil, text: "Chirp!", username: nil, hash_tags: [],
mentions: []}
iex> status.text
"Chirp!"
iex> status = %Tweet{status | text: "RT Chirp!"}
%Tweet{id: nil, text: "RT Chirp!", username: nil, hash_tags: [],
mentions: []}
iex> status.text
"RT Chirp!"
iex> %Tweet{status | text: "@elixir-lang rocks!", username: "afronski"}
Tweet{id: nil, text: "@elixir-lang rocks!", username: "afronski",
hash_tags: [], mentions: []}Only false and nil are coerced to false. All other values are considered are coerced to true.
Elixir supports the traditional if and unless keywords for control flow branching, but as we'll see, their use will be limited in favor of superior approaches.
iex> saved = true
true
iex> if saved, do: IO.puts("saved"), else: IO.puts("failed")
saved
:ok
iex> if saved do
...(3)> IO.puts "saved"
...(3)> else
...(3)> IO.puts "failed"
...(3)> end
saved
:ok
iex> unless saved do
...(4)> IO.puts "save failed"
...(4)> end
nilThe first two if examples demonstrate Elixir's inline and expanded expression syntax. In fact, the expanded, multi-line example is simply sugar for the the inline do: / else: syntax.
For cases where nesting or chaining ifs would be required, cond can be used instead to list multiple expressions and evaluate the first true match.
iex> temperature = 30
30
iex> cond do
...(2)> temperature >= 212 -> "boiling"
...(2)> temperature <= 32 -> "freezing"
...(2)> temperature <= -459.67 -> "absolute zero"
...(2)> end
"freezing"
case provides control flow based on pattern matching. Given an expression, case will match against each clause until the first pattern is matched. At least one pattern must be matched or CaseClauseError will be raised. Let's write a mini calculation parser to perform a few basic operations:
iex>
calculate = fn expression ->
case expression do
{:+, num1, num2} -> num1 + num2
{:-, num1, num2} -> num1 - num2
{:*, num1, 0} -> 0
{:*, num1, num2} -> num1 * num2
{:/, num1, num2} -> num1 / num2
end
end
#Function<6.17052888 in :erl_eval.expr/5>
iex> calculate.({:+, 8, 2})
10
iex> calculate.({:*, 8, 0})
0
iex> calculate.({:*, 8, 2})
16
iex> calculate.({:^, 8, 2})
** (CaseClauseError) no case clause matching: {:^, 8, 2}
iex>The calculate function accepts a three element tuple containing an atom to represent the operation to perform followed by two numbers. case is used to pattern match against the operation, as well as bind the the num1 and num2 variables for the matched clause.
An underscore in a match can serve as a "catch-all" clause:
iex>
calculate = fn expression ->
case expression do
{:+, num1, num2} -> num1 + num2
{:-, num1, num2} -> num1 - num2
{:*, num1, num2} -> num1 * num2
_ -> raise "Unable to parse #{inspect expression}"
end
end
#Function<6.17052888 in :erl_eval.expr/5>
iex> calculate.({:/, 10, 2})
** (RuntimeError) Unable to parse {:/, 10, 2}Guard Clauses can be used to restrict a pattern from matching based on a condition or set of conditions. Consider an extension to our calculation parser where dividing by zero should never occur:
iex>
calculate = fn expression ->
case expression do
{:+, num1, num2} -> num1 + num2
{:-, num1, num2} -> num1 - num2
{:*, num1, num2} -> num1 * num2
{:/, num1, num2} when num2 != 0 -> num1 / num2
end
end
#Function<6.17052888 in :erl_eval.expr/5>
iex> calculate.({:/, 10, 2})
5.0
iex> calculate.({:/, 10, 0})
** (CaseClauseError) no case clause matching: {:/, 10, 0}The Virtual Machine supports a limited set of guard expressions:
- Comparison, boolean, and arithmetic operators:
==,!=,===,!==,>,<,<=,>=and,or,not,!+,-,*,/- Examples:
def credit(balance, amt) when amt > 0def debit(balance, amt) when amt > 0 and balance >= amt
- Concatenation operators, providing the first term is a literal:
<>,++
- The
inoperator:- examples
def grade(letter) when letter in ["A", "B"]
- examples
- Type checking functions:
is_atom/1is_binary/1is_bitstring/1is_boolean/1is_exception/1is_float/1is_function/1is_function/2is_integer/1is_list/1is_number/1is_pid/1is_port/1is_record/1is_record/2is_reference/1is_tuple/1
- Top-level functions:
absbit_sizebyte_sizedivelemfloathdlengthnodenoderemroundselfsizetltrunctuple_size
A string is a UTF-8 encoded binary. In order to understand exactly what we mean by that, we need to understand the difference between bytes and code points.
The Unicode standard assigns code points to many of the characters we know. For example, the letter a has code point 97 while the letter ł has code point 322. When writing the string "hełło" to disk, we need to convert this sequence of characters to bytes. If we adopted a rule that said one byte represents one code point, we wouldn't be able to write "hełło", because it uses the code point 322 for ł, and one byte can only represent a number from 0 to 255. But of course, given you can actually read "hełło" on your screen, it must be represented somehow. That's where encodings come in.
When representing code points in bytes, we need to encode them somehow. Elixir chose the UTF-8 encoding as its main and default encoding. When we say a string is a UTF-8 encoded binary, we mean a string is a bunch of bytes organized in a way to represent certain code points, as specified by the UTF-8 encoding.
Since we have characters like ł assigned to the code point 322, we actually need more than one byte to represent them. That's why we see a difference when we calculate the byte_size/1 of a string compared to its String.length/1:
iex> string = "hełło"
"hełło"
iex> byte_size(string)
7
iex> String.length(string)
5There, byte_size/1 counts the underlying raw bytes, and String.length/1 counts characters.
Note: if you are running on Windows, there is a chance your terminal does not use UTF-8 by default. You can change the encoding of your current session by running
chcp 65001before enteringiex(iex.bat).
UTF-8 requires one byte to represent the characters h, e, and o, but two bytes to represent ł. In Elixir, you can get a character's code point by using ?:
iex> ?a
97
iex> ?ł
322You can also use the functions in the String module to split a string in its individual characters, each one as a string of length 1:
iex> String.codepoints("hełło")
["h", "e", "ł", "ł", "o"]You will see that Elixir has excellent support for working with strings. It also supports many of the Unicode operations. In fact, Elixir passes all the tests showcased in the article "The string type is broken".
However, strings are just part of the story. If a string is a binary, and we have used the is_binary/1 function, Elixir must have an underlying type empowering strings. And it does! Let's talk about binaries.
In Elixir, you can define a binary using <<>>:
iex> <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> byte_size(<<0, 1, 2, 3>>)
4A binary is a sequence of bytes. Those bytes can be organized in any way, even in a sequence that does not make them a valid string:
iex> String.valid?(<<239, 191, 19>>)
falseThe string concatenation operation is actually a binary concatenation operator:
iex> <<0, 1>> <> <<2, 3>>
<<0, 1, 2, 3>>A common trick in Elixir is to concatenate the null byte <<0>> to a string to see its inner binary representation:
iex> "hełło" <> <<0>>
<<104, 101, 197, 130, 197, 130, 111, 0>>Each number given to a binary is meant to represent a byte and therefore must go up to 255. Binaries allow modifiers to be given to store numbers bigger than 255 or to convert a code point to its UTF-8 representation:
iex> <<255>>
<<255>>
iex> <<256>> # truncated
<<0>>
iex> <<256 :: size(16)>> # use 16 bits (2 bytes) to store the number
<<1, 0>>
iex> <<256 :: utf8>> # the number is a code point
"Ā"
iex> <<256 :: utf8, 0>>
<<196, 128, 0>>If a byte has 8 bits, what happens if we pass a size of 1 bit?
iex> <<1 :: size(1)>>
<<1::size(1)>>
iex> <<2 :: size(1)>> # truncated
<<0::size(1)>>
iex> is_binary(<<1 :: size(1)>>)
false
iex> is_bitstring(<<1 :: size(1)>>)
true
iex> bit_size(<< 1 :: size(1)>>)
1The value is no longer a binary, but a bitstring -- a bunch of bits! So a binary is a bitstring where the number of bits is divisible by 8.
iex> is_binary(<<1 :: size(16)>>)
true
iex> is_binary(<<1 :: size(15)>>)
falseWe can also pattern match on binaries / bitstrings:
iex> <<0, 1, x>> = <<0, 1, 2>>
<<0, 1, 2>>
iex> x
2
iex> <<0, 1, x>> = <<0, 1, 2, 3>>
** (MatchError) no match of right hand side value: <<0, 1, 2, 3>>Note each entry in the binary pattern is expected to match exactly 8 bits. If we want to match on a binary of unknown size, it is possible by using the binary modifier at the end of the pattern:
iex> <<0, 1, x :: binary>> = <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> x
<<2, 3>>Similar results can be achieved with the string concatenation operator <>:
iex> "he" <> rest = "hello"
"hello"
iex> rest
"llo"A complete reference about the binary / bitstring constructor <<>> can be found in the Elixir documentation. This concludes our tour of bitstrings, binaries and strings. A string is a UTF-8 encoded binary and a binary is a bitstring where the number of bits is divisible by 8. Although this shows the flexibility Elixir provides for working with bits and bytes, 99% of the time you will be working with binaries and using the is_binary/1 and byte_size/1 functions.
A charlist is nothing more than a list of code points. Char lists may be created with single-quoted literals:
iex> 'hełło'
[104, 101, 322, 322, 111]
iex> is_list 'hełło'
true
iex> 'hello'
'hello'
iex> List.first('hello')
104You can see that, instead of containing bytes, a charlist contains the code points of the characters between single-quotes (note that by default IEx will only output code points if any of the integers is outside the ASCII range). So while double-quotes represent a string (i.e. a binary), single-quotes represent a charlist (i.e. a list).
In practice, charlists are used mostly when interfacing with Erlang, in particular old libraries that do not accept binaries as arguments. You can convert a charlist to a string and back by using the to_string/1 and to_charlist/1 functions:
iex> to_charlist "hełło"
[104, 101, 322, 322, 111]
iex> to_string 'hełło'
"hełło"
iex> to_string :hello
"hello"
iex> to_string 1
"1"Note that those functions are polymorphic. They not only convert charlists to strings, but also integers to strings, atoms to strings, and so on.
Our application is a very simple HTTP API that returns air quality metrics and current weather conditions for a given location inside a JSON object. Air quality metrics are fetched from two independent services - GIOŚ and Airly. For weather we are using Yahoo Weather API.
Of course in order to avoid violating the terms of services and rate limits our API employs additional caching layer and small offline preprocessing jobs.
- String module documentation.
- What is geohashing?
$ git checkout TASK_EX_1$ git checkout TASK_EX_2$ git reset .
$ git checkout .
$ git checkout master