Elixir School - Basics
Basics
Atoms
A constant whose name is its value
:foo
Equivalents:
-
JavaScript
const s = Symbol("foo")
-
Ruby
:foo
:foo
:foo == :bar
Booleans true
and false
are also atoms :true
and :false
[is_atom(true), is_atom(false), is_boolean(true), is_boolean(false)]
Module names are also valid atoms
Boolean Operators
||
, &&
, !
, and
, or
, not
are the built-in boolean operators
-20 || true
42 && true
!42
Operators and
, or
, not
must have a boolean as their first argument
true and 42
42 and true
not false
String Interpolation and Concatenation
first_name = "Charles"
middle_name = "Thomas"
last_name = "Roth"
full_name = "#{first_name} #{middle_name}" <> " #{last_name}" <> " III"
Collections
List
Collections of values which may include multiple types and non-unique values
Implemented as linked lists therefore prepend is O(1)
and append is O(n)
list = [3.14, :pie, "Apple"]
# prepend (fast)
["π" | list]
# append (slow), concatenation
list ++ ["π"]
# list subtraction
list -- ["Apple"]
# head & tail
[head | tail] = list
h = hd(list)
t = tl(list)
[h, t] == [head, tail]
Tuples
Similar to List
- Stored contiguously in memory
- Modification is expensive (new tuple is copied to memory)
Very useful in “pattern matching”
tup_list = {3.14, :pie, "Apple"}
Keyword List
- Two-element tuple whose first element is an atom
- Keys are atoms, ordered and do not have to be unique
[first_name: "Charlie", last_name: "Roth"]
[{:first_name, "Charlie"}, {:last_name, "Roth"}]
Maps
- key-value store
- Keys can be of any type
- Un-ordered
person = %{:first_name => "Charlie", :last_name => "Roth", :age => 26}
person[:first_name]
A Map
can be updated with “map update” syntax
%{person | :first_name => "Charles"}
When modifying a map, with “map update” syntax, only keys that are present are allowed to be updated
%{person | :location => "Sweden"}
To add keys that are not currently in the Map
, use the Map.put/3
function
Map.put(person, :location, "Sweden")
Enum
Enum
module includes 70 functions to work with enumerables (Map
, List
, Keyword List
)
Lazy enumeration can be achieved with the Stream
module
Enum.all?(["foo", "bar", "hello"], fn s -> String.length(s) == 3 end)
Enum.any?(["foo", "bar", "hello"], fn s -> String.contains?(s, "oo") end)
Enum.each(["one", "two", "three"], fn s -> IO.puts(s) end)
Enum.map([1, 2, 3], fn n -> n * 2 end)
Enum.filter([0, 1, 2, 3, 4, 5], fn n -> rem(n, 2) == 0 end)
Enum.reduce([1, 2, 3, 4, 5], 0, fn n, acc -> acc + n end)
Enum.reduce(["3", "2", "1", "Lift Off!!"], "", fn s, acc -> acc <> " " <> s end)
Enum.sort([4, 2, 5, 3, 1])
Enum.sort([4, 2, 5, 3, 1], :desc)
The “Capture Operator” &
can be used to represent an anonymous function
Arguments supplied to the anonymous function can be accessed through &1 ... &n
where n
represents the order of the arguments
Enum.reduce(["3", "2", "1", "Lift Off!!"], "", &(&2 <> " " <> &1))
Pattern Matching
Allows matching of simple values, data structures and functions
Match Operator
In Elixir, the =
sign is not the same as most programming languages. The =
sign is actually an operator called the match operator. Behaves as an algebraic =
sign and when the match is successful, it behaves as assignment.
The =
turns the expression into an equation where the LHS must match the RHS. If the match succeeds, it returns the value of the equation
x = 1
1 = x
2 = x
list = [1, 2, 3]
[1, 2, 3] = list
[2, 3, 4] = list
[1 | tail] = list
{:ok, value} = {:ok, "Success"}
{:ok, value} = {:error, "Failed to read file"}
Pin Operator
The ^
operator matches on the existing value rather than rebinding to a new one
Keys in Map
s and function clasues can use the ^
operator as well
x = 1
^x = 2
{x, ^x} = {2, 1}
x
key = "hello"
%{^key => "world"} = %{"hello" => "world"}
greeting = "Hello"
greet = fn
^greeting, name -> "Hi #{name}"
greeting, name -> "#{greeting}, #{name}"
end
greet.("Hello", "Charlie")
greet.("God morgon", "Charlie")
Control Structures
Control structures in Elixir are implemented as functions such as if/2
where the first argument is the conditional to evaluate and the second argument is the continuation function. However, in practice they are used as macros and not the typical language constructs like in most programming languages.
Available control structures:
-
if/2
-
unless/2
-
case/2
-
cond/1
-
with/1
if String.valid?("Hello") do
"Valid string :)"
else
"Invalid string :("
end
unless is_integer(123) do
"Not an integer"
end
case/2
is the same as Rust’s match
operator
case {:ok, "Hello world"} do
{:ok, msg} -> msg
{:error} -> "Oops"
_ -> "idk what is going on"
end
case {1, 2, 3} do
{1, x, 3} when rem(x, 2) == 0 ->
"Matched"
_ ->
"Second number is odd"
end
The with/2
function is useful when you want to perform something similar to nested case/2
functions but desire a more composable way to write these conditions
person = %{first: "Charles", last: "Roth"}
with {:ok, first} <- Map.fetch(person, :first),
{:ok, last} <- Map.fetch(person, :last),
do: last <> ", " <> first
person = %{first: "Charles"}
with {:ok, first} <- Map.fetch(person, :first),
{:ok, last} <- Map.fetch(person, :last),
do: last <> ", " <> first
Functions
Anonymous Functions
adder = fn a, b -> a + b end
adder.(2, 3)
adder_short = &(&1 + &2)
adder_short.(4, 5)
Pattern matching can be used with function signatures as well
handle_result = fn
{:ok, result} -> IO.puts("Handling...")
{:ok, _} -> IO.puts("Handling some other stuff...")
{:error} -> IO.puts("Error occurred")
end
some_result = 42
handle_result.({:ok, some_result})
handle_result.({:error})
Named Functions
defmodule Greeter do
def greet(name) do
"Hey, " <> name
end
def goodbye(name), do: "Good bye, " <> name
end
Greeter.greet("Charlie")
Greeter.goodbye("Charlie")
Functions have an arity, such as case/2
, where /2
represents the number arguments the function accepts
This allows you to have several functions with the same name but with a different number of arguments
defmodule Greeter2 do
def hello(), do: "Hello, anon"
def hello(name), do: "Hello, " <> name
def hello(name1, name2), do: "Hello, " <> name1 <> " and " <> name2
end
IO.puts(Greeter2.hello())
IO.puts(Greeter2.hello("Charlie"))
IO.puts(Greeter2.hello("Charlie", "eilrahC"))
Multiple functions with the same name but different arities is actually an example of pattern matching, a simple one that is. Pattern matching in function signatures can be more powerful than just the number of arguments, they can be matched by the contents of the arguments themselves
defmodule Greeter1 do
def hello(%{name: person_name}) do
IO.puts("Hello, " <> person_name)
end
def full_hello(%{name: person_name} = person) do
IO.puts("Hello, " <> person_name)
IO.inspect(person)
end
end
Here the function Greeter1.hello/1
takes a Map
as its argument. We then destructure this Map
and extract the value at the key :name
and assign that to the variable person_name
. This assignment only happens if the function signature match is correct, else the result is an error
Greeter1.hello(%{name: "Charlie Roth"})
Greeter1.hello(%{name: "Miranda Nichols"})
# partial pattern match of the passed Map
Greeter1.hello(%{name: "Parker Roth", location: "Arizona", age: 21})
Greeter1.hello("Charlie Roth")
Greeter1.full_hello(%{name: "Parker Roth", location: "Arizona", age: 21})
Guards
Functions can have “attached” guard statements that will be checked after the function signature is matched
This allows you to have even more granular matching on which function to execute
defmodule Greeter3 do
def hello(names) when is_list(names) do
names |> Enum.join(", ") |> hello
end
def hello(name) when is_binary(name) do
phrase() <> name
end
defp phrase, do: "Hello "
end
Greeter3.hello(["Charlie", "Miranda", "Parker"])
Function signatures can have default values for arguments when they are not supplied to the function
defmodule Greeter4 do
def hello(name, lang \\ "en") do
phrase(lang) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("es"), do: "Hola, "
end
IO.puts(Greeter4.hello("Charlie"))
IO.puts(Greeter4.hello("Charlie", "es"))
Pipe Operator
Best practice when using the |>
operator is when the arity of a function is more than 1, use parentheses. Otherwise just writing the function name like String.split
is fine
"Charles Thomas Roth" |> String.split()
"Charles Thomas Roth" |> String.upcase() |> String.split()
Modules
Modules are Elixir’s way to organize functions into namespaces
There are some basic examples of modules above
Nested Modules
defmodule Example.Greetings do
def morning(name) do
"Good morning #{name}"
end
def evening(name) do
"Good evening #{name}"
end
end
Example.Greetings.morning("Charlie")
Module Attributes
Commonly used as constants that are scoped to the module they are within
Reservered module attributes in Elixir:
-
moduledoc
- Documents the current module -
doc
- Documentation for functions and macros -
behaviour
- Use an OTP for user-defined behaviour
defmodule Example do
@greeting "Hello"
def greeting(name) do
~s(#{@greeting} #{name})
end
end
Example.greeting("Charlie")
Structs
Special maps with a defined set of keys and default values
Defined within a module which it inherits the name from
defmodule Example.User do
defstruct name: "Charlie", roles: []
end
%Example.User{}
%Example.User{name: "Miranda", roles: ["Physicist"]}
u = %Example.User{}
u = %{u | name: "Charles"}
u = %{u | roles: ["Programmer", "Guru"]}
Module Composition
The alias
macro enables the reference and make use of one module inside of another
defmodule Sayings.Greeting do
def basic(name), do: "Hello, " <> name
end
defmodule Sayings.Goodbye do
def basic(name), do: "Goodbye, " <> name
end
defmodule Example do
alias Sayings.Greetings
def greeting(name), do: Greetings.basic(name)
end
When aliasing a module that has a possible conflict with the current module or another module, or you just want to shorten the referencing name
defmodule Example do
alias Sayings.Greeting, as: Hi
def print_msg(name), do: Hi.basic(name)
end
Since modules are namespaces and the author has the ability to “nest” modules, it is possible alias
multiple modules from the “parent” modules
defmodule Example do
alias Sayings.{Greeting, Goodbye}
def print_msg(name) do
name |> Greeting.basic() |> IO.puts()
name |> Goodbye.basic() |> IO.puts()
end
end
Example.print_msg("Charlie")
The import
macro adds the functions of the module it is importing to the scope of the module
# last/1 is not in the scope of the program's module
last([1, 2, 3])
import List
last([1, 2, 3])
The require
macro does the same thing as import
except only for macros of the module it is referring to
The use
macro can add functionality to a module that is using it
The module that is referenced by the use
macro is required to have a callback __using__/1
defined
This will be called and add the functionality provided in this call back
This is a powerful feature of metaprogramming in Elixir but should be used wisely
Comprehensions
Comprehensions can often be used to produce more concise statements for Enum
and Stream
iteration
# lists
for x <- [1, 2, 3, 4, 5], do: x * x
# keyword lists
for {_key, val} <- [one: 1, two: 2, three: 3], do: val
# maps
for {k, v} <- %{"a" => "A", "b" => "B", "c" => "C"}, do: {k, v}
# binaries
for <>, do: <>
# multiple generators
for n <- [1, 2, 3, 4], times <- 1..n do
String.duplicate("*", times)
end
“Filters” can be applied to comprehensions in a similar fashion to guards
import Integer
# all even numbers between 1 and 10
for x <- 1..10, is_even(x), do: x
# all even numbers, between 1 and 100, that are also a multiple of 3
for x <- 1..100,
is_even(x),
rem(x, 3) == 0,
do: x
Listen comprehensions can also be useful to transform data
This can be accomplished by using :into
# create a map from a keyword list
for {k, v} <- [one: 1, two: 2, three: 3], into: %{}, do: {k, v}
# create a binary from a integer list
for c <- [72, 101, 108, 108, 111], into: "", do: <>