This is a short list of Elixir basics made based on the great official Elixir introduction found at http://elixir-lang.org/getting-started/introduction.html
For insight on what prompted the creation of this document, check out Elixir, your shortcut to the world of Erlang.
Most snippets can be pasted to iex
, the REPL (see below), evaluation result
is displayed as comment below, i.e. returned true
would be # > true
-
Dynamically typed, immutable data types, loops with recursion
-
Runs on Erlang VM, can call Erlang code with no overhead
-
Parallelism by message passing and memory-isolated green threads ("processes")
-
Variables can be assigned multiple times (unlike in Erlang)
-
iex
for REPL,elixir <file>
to evaluate script,elixirc
is the compiler,mix
is the build tool -
Hex is the package repository https://hex.pm/ used by
mix
-
File naming convention:
.ex
for compiled and.exs
for script files -
#
for comment -
IO.puts "print screen"
,IO.inspect(data_to_dump_on_screen)
,inspect(data_to_string)
-
Parenthesis are optional in unambiguous situations
-
Foo.bar/2
means function named "bar" of module "Foo" with arity 2 -
In REPL
h div
prints documentation fordiv
,h Kernel
forKernel
module -
?
suffix convention for function names returning boolean, e.g.even?/1
instead ofis_even/1
-
No line termination.
;
allows joining lines:x = 1; y = x + 1; y == 2
-
Strings UTF-8
"
double quote is for strings'
single quote is char list<<0, 1, 2>>
is byte sequence~s({"json": "string"})
for "triple-quote" string, i.e. no"
escaping
-
Data structures
-
[]
for linked list (prepending is fast[1] ++ []
) -
{}
for tuple -
:foo
for atom/symbol, -
[a: 1, b: 2]
for keyword list (equal to[{:a, 1}, {:b 2}]
), keys must be atoms, ordered, can have same key twice -
%{:a => 1, :b => 2}
for traditional map, any value as key, no ordering, no duplicate keys (last assignment overwrites)- with atom-only keys,
%{a: 1, b: 2}
shorthand can be used - pattern-match-able:
%{:k => v} = %{k: 1, k2: 2}; v == 1
- with atom-only keys,
-
Maps and keyword lists implement
Dict
interface -
Structs are extended maps with default values getting the name of their module
defmodule Struct do defstruct foo: "string", bar: true end %Struct{} == %Struct{foo: "string", bar: true} # > true %Struct{foo: "baz"} == %Struct{foo: "baz", bar: true} # > true
with compile-time checks
%Struct{baz: true} # CompileError, unknown key
and can be pattern matched like maps%Struct{foo: var} = %Struct{}; var == "string"
- Cloning struct with different defaults (and no new keys) using
structural sharing, use
|
pipe:lower = %Struct{}; upper = %Struct{lower | foo: "STRING"}
- Cloning struct with different defaults (and no new keys) using
structural sharing, use
-
-
a..b
for ranges,Enum.map(1..3, fn x -> x * 2 end) == [2, 4, 6]
, enumerable is a protocol -
add = fn a, b -> a + b end
anonymous function, calledadd.(1,2)
-
convention
*size
named functions constant time,*length
require iteration -
"interpolate #{variable}"
,"concat" <> "string"
-
FYI, comparing different data types OK. Valid:
1 < :atom
. Precedence from docs. -
Pattern matching
_
discard variable (not readable){i, s, a} = {1, "string", :atom}
{:ok, result} = {:ok, 13}
->result == 13
{:ok, result} = {:error, 13}
-> exception[head | tail] = [1, 2, 3]
->head == 1 and tail == [2, 3]
-
Pin
^
does in-place assert:x = 1; {x, ^x} = {2, 1}; x == 2
-
Functions must be defined inside modules, last statement produces return value
defmodule Math do def sum(a, b) do a + b end end Math.sum(1, 2) # > 3
creates
Math.sum/2
-
Erlang code can be accessed by their atomic names
:math.pow(2, 2) # > 4.0
-
def/2
for public (visible to other modules),defp/2
for private functions -
captured_fn = &Math.sum/2; captured_fn.(1, 2) == 3
-
&(&1 + 1)
,&n
is nth, argument, statement equalsfn x -> x + 1 end
- i.e.
&List.flatten(&1, &2)
equalsfn(list, tail) -> List.flatten(list, tail) end
- i.e.
-
Default arguments, can also be statements (evaluated on call)
def fun(x \\ IO.puts "hello world") do x end
-
A parallel "process" is started with
spawn
, each haspid
for addressing the process. Currentpid
read withself()
.- Message are sent to
pid
withsend
, e.g.send self(), {:some "data}
receive
blocks and waits for pattern matched messages
pid = spawn fn -> str = receive do {:msg_a, data} -> "Got tuple matching :msg_a with #{data}" "foo" -> "Got string \"foo\"" some_var -> "Caught something else: #{some_var}" after 1000000 -> "timeout, didn't get anything in 1000ms" end IO.puts str end send(pid, {:msg_a, "foo"}) # > Got tuple matching :msg_a with foo
pid
can be named withProcess.register(pid, :name_for_pid)
- Message are sent to
-
Guards
-
In
case
case {1, 2} do {1, x} when x < 3 -> "guard matched #{x}" {1, x} -> "guard passed #{x}" _ -> "match rest" end # > "guard matched 2"
-
In function definition used for dispatching
defmodule Guard do def func(a) when a < 0 do "#{a} less than 0" end def func(a) when a > 0 do "#{a} greater than 0" end end Guard.func(1) # > "1 greater than 0" Guard.func(-1) # > "-1 less than 0" Guard.func(0) # > ** (FunctionClauseError) no function clause matching in Guard.func/1
-
-
cond
to do if then else, if none match, error raisedcond do 1 + 1 == 3 -> "if" 1 + 1 == 4 -> "else if" true -> "else" end # > "else"
-
do/end
:if true do: (expr, expr, ...)
same asif true do expr; expr; ... end
-
Stream
for lazy enumeration,Enum
for eager -
|>
for pipe, prepends operator LHS (left-hand side) to argument list of RHS function# List.delete(list, item) [1, 2] |> List.delete(1) # > [2] [1, 2, 3] |> Enum.map(fn x -> x + 1 end) |> Enum.filter(fn x -> x < 3 end) |> Enum.sum # > 2
- Elixir functions try to keep operated value as functions' first argument to promote pipe operator's usage
-
Protocol (interface in some other languages) defines prototype for implementation
defprotocol Validation do @fallback_to_any true def valid?(data) end defimpl Validation, for: Integer do def valid?(i), do: i > 0 end Validation.valid?(-3) # > false
Fallback for unexpected data types with
@fallback_to_any true
annotation beforedefprotocol
prototype allows.defimpl Validation, for: Any, do: (def valid?(i), do: false)
-
for
comprehension is syntactic sugar for enumerationsresult = for x <- [1,2,3], # "generator" y = x + 1, # temporary variables can be used y > 2, # "filter" y < 4, # another filter do: y + 1 # "collectable" # > [4]
Generator is not same as e.g. Python generator (it can still be enumerable lazy stream though). It's just the part producing values.
-
~
for sigil, allowing language extensions, defined with function prototypesigil_{identifier}
. Predefined sigils include e.g. regexps (sigil_r
)"foo" =~ ~r/foo|bar/
-
Status is usually returned as atom in tuple...
File.read("hello") # > {:error, :enoent}
...instead of raising (throwing) an error (exception).
try do raise "fail" rescue e in RuntimeError -> e end # > %RuntimeError{message: "fail"}
Use
defexception
to create own exception types. Errors are meant for unexpected situations, not for control flow. Process supervision is the general way to protect against worst case situations. -
try
/catch
can be used to return values from bad API / nested structure / function callback loop, should not be used unless it's the only feasible option. -
exit
signals supervisor