- Get some help:
iex> i 'hello'
Term
'hello'
Data type
List
Description
...
Raw representation
[104, 101, 108, 108, 111]
Reference modules
List
- When “counting” the number of elements in a data structure, Elixir also abides by a simple rule: the function is named size if the operation is in constant time (i.e. the value is pre-calculated) or length if the operation is linear (i.e. calculating the length gets slower as the input grows).
- String concatenation is done with:
<>. - Operators
or,and,notcan only accept boolean values. Besides these boolean operators, Elixir also provides||,&&and!which accept arguments of any type. For these operators, all values exceptfalseandnilwill evaluate totrue. - The variable
_is special in that it can never be read from. Trying to read from it gives an unbound variable error. - Guard Clauses are neat:
# Anonymous functions can have guard clauses:
# They also apply to the 'case' statement, 'when'.
iex> f = fn
...> x, y when x > 0 -> x + y
...> x, y -> x * y
...> end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> f.(1, 3)
4
iex> f.(-1, 3)
-3
All good, just true and false. Nothing special.
Pretty much like Lisp's Atoms, A.K.A. Symbols in Ruby.
Functions delimited between fn and end.
# first class citizens
iex> add = fn a, b -> a + b end
iex> add.(3, 2)
Describes itself.
# Add or subtract using ++ or --
iex> [2, 23, 42, 11, true]
iex> list = [1, 2, 3]
# Get head and tail.
iex> hd(list)
1
iex>tl(list)
[2, 3]
- When Elixir sees a list of printable ASCII numbers, Elixir will print that as a char list (literally a list of characters).
- Single-quotes are char lists, double-quotes are strings.
Similar to lists, but stored in memory, all data is availible with no recursion needed.
iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2
Accessing the length of a list is a linear operation: we need to traverse the whole list in order to figure out its size. Updating a list is fast as long as we are prepending elements.
Tuples, on the other hand, are stored contiguously in memory. This means getting the tuple size or accessing an element by index is fast. However, updating or adding elements to tuples is expensive because it requires copying the whole tuple in memory.
When “counting” the number of elements in a data structure, Elixir also abides by a simple rule: the function is named size if the operation is in constant time (i.e. the value is pre-calculated) or length if the operation is linear (i.e. calculating the length gets slower as the input grows).
The match operator is not only used to match against simple values, but it is also useful for destructuring more complex data types. For example, we can pattern match on tuples:
iex> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> a
:hello
iex> b
"world"
A list also supports matching on its own head and tail:
iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]
The pin operator ^ should be used when you want to pattern match against an existing variable’s value rather than rebinding the variable.
Behavious pretty much as the classic case statement.
iex> case {1, 2, 3} do
...> {4, 5, 6} ->
...> "This clause won't match"
...> {1, x, 3} ->
...> "This clause will match and bind x to 2 in this clause"
...> _ ->
...> "This clause would match any value"
...> end
If you want to pattern match against an existing variable, you need to use the ^ operator:
iex> x = 1
1
iex> case 10 do
...> ^x -> "Won't match"
...> _ -> "Will match"
...> end
Another cool example, now with clauses conditions:
iex> case {1, 2, 3} do
...> {1, x, 3} when x > 0 ->
...> "Will match"
...> _ ->
...> "Would match, if guard condition were not satisfied"
...> end
- If none of the clauses match, an error is raised.
case is useful when you need to match against different values. However, in many circumstances, we want to check different conditions and find the first one that evaluates to true. In such cases, one may use cond.
iex> cond do
...> 2 + 2 == 5 ->
...> "This will not be true"
...> 2 * 2 == 3 ->
...> "Nor this"
...> 1 + 1 == 2 ->
...> "But this will"
...> end
- This is equivalent to
elseandifclauses in many imperative languages. - If none of the conditions return
true, an error is raised. For this reason, it may be necessary to add a final condition, equal totrue, which will always match.
Are useful when you need to check for just one condition, also pro provides a else statement.
iex> if true do
...> "This works!"
...> end
"This works!"
iex> unless true do
...> "This will never be seen"
...> end
nil
Equivalent to { / }, it's also possible things like:
iex> if false, do: :this, else: :that
# Expressions like:
iex> is_number if true do
...> 1 + 2
...> end
** (CompileError) undefined function: is_number/2
# Should be:
iex> is_number(if true do
...> 1 + 2
...> end)
true
You can define a binary using <<>>. It's just a sequence of bytes.
- The string concatenation operation is actually a binary concatenation operator
<>. - A binary is a bitstring where the number of bits is divisible by 8. Smaller bit are just bitstrings!
- A string is a UTF-8 encoded binary, and a binary is a bitstring where the number of bits is divisible by 8.
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>>
A char list is nothing more than a list of characters.
Char list contains the code points of the characters between single-quotes (note that IEx will only output code points if any of the chars is outside the ASCII range). So while double-quotes represent a string (i.e. a binary), single-quotes represents a char list (i.e. a list).
It's a associative data structure. In Elixir, when we have a list of tuples and the first item of the tuple (i.e. the key) is an atom, we call it a keyword list:
iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
iex> list[:a]
1
- It's the default mechanism for passing options to functions in Elixir.
- Only allows Atoms as keys.
- Ordered as specified by the developer.
- Remember, though, keyword lists are simply lists, and as such they provide the same linear performance characteristics as lists. The longer the list, the longer it takes to read from. For bigger data use maps instead.
Whenever you need a key-value store, maps are the “go to” data structure in Elixir:
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
iex> map[:c]
nil
- Allows any value as key.
- Maps’ keys do not follow any ordering.
Interacts great with pattern matching:
iex> %{} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}
Better syntax when all keys are atoms:
iex> map = %{a: 1, b: 2}
Another interesting property of maps is that they provide their own syntax for updating and accessing atom keys:
iex> map = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> map.a
1
iex> map.c
** (KeyError) key :c not found in: %{2 => :b, :a => 1}
iex> %{map | :a => 2}
%{:a => 2, 2 => :b}
iex> %{map | :c => 3}
** (KeyError) key :c not found in: %{2 => :b, :a => 1}
iex> users = [
john: %{name: "John", age: 27, languages: ["Erlang", "Ruby", "Elixir"]},
mary: %{name: "Mary", age: 29, languages: ["Elixir", "F#", "Clojure"]}
]
We have a keyword list of users where each value is a map containing the name, age and a list of programming languages each user likes. If we wanted to access the age for john, we could write:
iex> users[:john].age
27
It happens we can also use this same syntax for updating the value:
iex> users = put_in users[:john].age, 31
[john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}]
The update_in/2 macro is similar but allows us to pass a function that controls how the value changes. For example, let’s remove “Clojure” from Mary’s list of languages:
iex> users = update_in users[:mary].languages, &List.delete(&1, "Clojure")
[john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
mary: %{age: 29, languages: ["Elixir", "F#"], name: "Mary"}]
There is more to learn about put_in/2 and update_in/2, including the get_and_update_in/2 that allows us to extract a value and update the data structure at once. There are also put_in/3, update_in/3 and get_and_update_in/3 which allow dynamic access into the data structure. Check their respective documentation in the Kernel module for more information.
In Elixir we group several functions into modules.
iex> defmodule Math do
...> def sum(a, b) do
...> a + b
...> end
...> end
iex> Math.sum(1, 2)
3
Given a file math.ex:
defmodule Math do
def sum(a, b) do
a + b
end
end
This file can be compiled using elixirc:
$ elixirc math.ex
This will generate a file named Elixir.Math.beam containing the bytecode for the defined module. If we start iex again, our module definition will be available (provided that iex is started in the same directory the bytecode file is in).
Elixir projects are usually organized into three directories:
ebin- contains the compiled bytecodelib- contains elixir code (usually .ex files)test- contains tests (usually .exs files)
When working on actual projects, the build tool called mix will be responsible for compiling and setting up the proper paths for you.
.ex- files to be compiled.exs- files to run in scripted mode (Learning purposes)
Executing:
$ elixir math.exs
def/2- defines a functiondefp/2- defines a private function
Function declarations also support guards and multiple clauses. If a function has several clauses, Elixir will try each clause until it finds one that matches.
defmodule Math do
def zero?(0) do
true
end
def zero?(x) when is_integer(x) do
false
end
end
IO.puts Math.zero?(0) #=> true
IO.puts Math.zero?(1) #=> false
IO.puts Math.zero?([1, 2, 3]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0) #=> ** (FunctionClauseError)
Similar to constructs like if, named functions support both do: and do/end block syntax, as we learned do/end is just a convenient syntax for the keyword list format. For example, we can edit math.exs to look like this:
defmodule Math do
def zero?(0), do: true
def zero?(x) when is_integer(x), do: false
end
Can actually be used to retrieve a named function as a function type. (Given the file)
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function(fun)
true
iex> fun.(0)
true
Local or imported functions, like is_function/1, can be captured without the module:
iex> &is_function/1
&:erlang.is_function/1
iex> (&is_function/1).(fun)
true
Note the capture syntax can also be used as a shortcut for creating functions:
iex> fun = &(&1 + 1)
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> fun.(1)
2
The &1 represents the first argument passed into the function.
The above is the same as fn x -> x + 1 end. It's useful for short function definitions.
If you want to capture a function from a module, you can do &Module.function():
iex> fun = &List.flatten(&1, &2)
&List.flatten/2
iex> fun.([1, [[2], 3]], [4, 5])
[1, 2, 3, 4, 5]
&List.flatten(&1, &2) is the same as writing fn(list, tail) -> List.flatten(list, tail) end which in this case is equivalent to &List.flatten/2. You can read more about the capture operator & in the Kernel.SpecialForms documentation.
Named functions default arguments:
defmodule Concat do
def join(a, b, sep \\ " ") do
a <> sep <> b
end
end
IO.puts Concat.join("Hello", "world") #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
If a function with default values has multiple clauses, it is required to create a function head (without an actual body) for declaring defaults:
defmodule Concat do
def join(a, b \\ nil, sep \\ " ")
def join(a, b, _sep) when is_nil(b) do
a
end
def join(a, b, sep) do
a <> sep <> b
end
end
IO.puts Concat.join("Hello", "world") #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello") #=> Hello
- Default values won’t be evaluated during the function definition
- When using default values, one must be careful to avoid overlapping function definitions
Beautifully without mutating:
defmodule Recursion do
def print_multiple_times(msg, n) when n <= 1 do
IO.puts msg
end
def print_multiple_times(msg, n) do
IO.puts msg
print_multiple_times(msg, n - 1)
end
end
Recursion.print_multiple_times("Hello!", 3)
# Hello!
# Hello!
# Hello!
Let’s now see how we can use the power of recursion to sum a list of numbers:
defmodule Math do
def sum_list([head | tail], accumulator) do
sum_list(tail, head + accumulator)
end
def sum_list([], accumulator) do
accumulator
end
end
IO.puts Math.sum_list([1, 2, 3], 0) #=> 6
The process of taking a list and reducing it down to one value is known as a reduce algorithm and is central to functional programming.
What if we instead want to double all of the values in our list?
defmodule Math do
def double_each([head | tail]) do
[head * 2 | double_each(tail)]
end
def double_each([]) do
[]
end
end
$ iex math.exs
iex> Math.double_each([1, 2, 3]) #=> [2, 4, 6]
Here we have used recursion to traverse a list, doubling each element and returning a new list. The process of taking a list and mapping over it is known as a map algorithm.
Recursion and tail call optimization are an important part of Elixir. However, when programming in Elixir you will rarely use recursion as above. The Enum module, (next chapter), already provides many conveniences for working with lists. For instance, the examples above could be written as:
iex> Enum.reduce([1, 2, 3], 0, fn(x, acc) -> x + acc end)
6
iex> Enum.map([1, 2, 3], fn(x) -> x * 2 end)
[2, 4, 6]
Or, using the capture syntax:
iex> Enum.reduce([1, 2, 3], 0, &+/2)
6
iex> Enum.map([1, 2, 3], &(&1 * 2))
[2, 4, 6]