Elixir
March 26, 2023Install/Tools
brew install elixir
Docs
Books
Learn Functional Programming with Elixir
Repl
iex
start interactive session
c('filename')
load a file into session
Dependencies
- edit
mix.exs
defp deps do
[{dialyxir, "~> 0.5", only: [:dev], runtime: false}]
end
mix do deps.get, deps.compile
mix dialyzer
About Elixir
- Elixir runs on the BEAM VM (erlang)
- Erlang is compatible. Syntax is familiar to Ruby
- Concurrency up front
- Functional
- Speed
Functional Programming with Elxir
- immutable data
- functions
- declarative code
Pure Functions
- values are immutable
- fn result is only affected by the args
- fn doesn't generate effects beyond the value it returns
- predictable
- can return errors
- referential transparency: total.(100,8) == 8.0
Impure Functions
- may not return consistent results when given the same inputs
- may produce effects beyond the function's scope
- unpredictable
- fn are impure when they reference values outside of a function scope
- produces side effects
Working with pure/impure functions
- isolate pure from impure
- write more pure
- handle impure functions
Using values explicitly
- always pass values between fns
- clear inputs/outputs
Transforming values
|>
pipe operator- result is passed into the next function
Declarative code
- focuses on what is necessary to solve a problem
Values
atom
is a const, like a symbol. e.g. :true
, :false
, :nil
<>
is a string concat operator
and
, or
, not
are meant for boolean comparisons
&&
, ||
, !
compare truthy and falsy values
Naming
- vars are snake_case
- aliases start with a capital letter
- modules are Pascal case
Functions
- the last statement is the return value
- max of 255 parameters, best practice is less than 5
- first class citizens
- named functions are defined in modules
()
are optional
common functional pattern:
upcase = fn string -> String.upcase(string) end
upcase.("hello, world")
using capturing operator
upcase = &String.upcase
upcase.("hello, world")
Modules
- importing arity
import File, only: [write: 3, read: 2]
- explicit arity imports are preferred
Strings
#{my_var}
string interpolation
Matching
- Pattern matching in functional programming can control flow.
- Useful for assigning variables, unpacking values and making decisions.
- Operator raises a MatchError when it fails to match
When the variable is on the left:
- it's assignment
- will match everything, binding the value to the right side
- Binding to existing variables is called 'rebinding'
- To avoid rebinding use the pin operator
^
1 = 1
# 1
2 = 1
# MatchError
x = 1 # 1
1 = x # 1
2 = x # MatchError
x = 2 # rebind
^x = 2 # 2
^x = 1 # match error
Destructuring
- Matching parts of a string, use the
<>
operator the check a string - Can't use a variable on the left side of the
<>
operator
"Authentication: " <> credentials = "Authentication: Basic dXNlcjpwYXNz"
credentials # "Basic dXNlcjpwYXNz"
Tuples
- Functions might not be consistent when returning tuples, some may return an atom for the unsuccessful result and a tuple for the successful one.
- A good practice, a common one, is to return
{:ok, value}
for success and{:error, :error_type}
for failure. - Tuples must have a known length and use contiguous memory.
{a,b,c} = {4,5,6}
a # 4
Signaling success and failures
process_life = fn -> {:ok, 42} end
{:ok, answer} = process_life.()
IO.puts "The answer is #{answer}." # The answer is 42
Equals operators
=
is used for pattern matching==
is used when the values are equal===
is used to check value and type
1 = 1 # 1
2 = 1 # match error!
1 == 1.0 # true
2 == 1 # false
1.0 === 1.0 # true
1.0 === 1 # false
Example
user_input = IO.gets("Write your ability scrore:\n")
{ability_score, _} = Integer.parse(user_input)
ability_modifier = (ability_score - 10) / 2
IO.puts("Your ability modifier is #{ability_modifier}")
{ability_score, _} = Integer.parse(user_input)
isn't using a success atom, the tuple is enough. If the parsing is successful, the expression also binds the first element to the variable ability_score
and ignores the remaining text using the wildcard _
.
Matching lists
- For unknown length use a list. The lists are linked lists, each item contains a reference to the next item.
- A lists ends by linking to an empty list, turning it into a proper list.
- It's useful to avoid infinite loops by checking if the last item is an empty list and stopping recursion.
- Use
_
wildcard to ignore items |
splits the elements you care about from the rest.
[a,a,a] = [1,1,1] # the list must have three elements with the same value
[a,b,a] = [1,2,1] # [1,2,1]
[a,a,a] = ["apples", "apples", "apples"]
[a, a, "pineapples"] = ["apples", "apples", "pineapples"]
[head | tail] = [:a, :b, :c, :d]
head # :a
tail # [:b, :c, :d]
[head | tail] = [:a]
tail # []
[head | tail] = [] # match error
[a,b | rest] = [1,2,3,4]
a # 1
b # 2
rest # [3,4]
Matching Maps
- Maps are key/value pairs
%{}
is used to create map values=>
is an alternative syntax, more verbose but can store an value as a key%{strength: strength_value} = abilities
pattern match object value%{}
will match all maps
user_signup = %{email: "johndoe@mail.com", password: "12345678"}
user_signup = %{:email => "johndoe@mail.com", :password => "12345678"} # equivalent
abilities = %{strength: 16, dexterity: 12, intelligence: 10}
%{strength: strength_value} = abilities
strength_value # 16
# matching and setting at the same time
%{intelligence: 10, dexterity: dexterity_value} = abilities
dexterity_value # 12
# check and bind at the same time, one step use for simple assignments
# basic filter
%{strength: strength_value = 16} = abilities
strength_value # 16
# two step version, use when you have some calculation or function call on the variables
strength_value = 16
%{strength: ^strength_value} = abilities
Maps vs Keyword Lists
- A keyword list is a list of two-element tuples, it allows duplicate keys but they must be atoms
- keywords are useful for function options, e.g. the import directive takes a keyword list because named functions can have identical names with different arity
- keyword lists permit you to create structures with identical keys but different values
- maps are useful for things representing database rows, because column names are unique
- the syntax of maps and keywords is very similar, but their limitations make them handy for different use cases
[b,c] = [a: 1, a: 12]
b # {:a, 1}
c # {:a, 12}
import String, only: [pad_leading: 2, pad_leading: 3]
pad_leading("def", 6) # " def"
pad_leading("def", 6, "-") # "---def"
x = %{a: 1, a: 12} # {a:12}
x = [a: 1, a: 12] # OK
x = [{:a, 1}, {:a, 12}] # same as above
x = %{1 => :a, 2 => :b} # ok
x = [1 => :a, 2 => :b] # syntax error
Matching Structs
- structs are extensions of mapping structures
- useful for representing consistent structures that have the same set of keys everywhere
- All structs have a list of permitted attributes
- Not possible to use a key that's not in the list of allowed attributes
- Only functional difference between structs and maps, the name of the struct can be used to indicate which type of structure we're expecting
date = ~D[2018-01-01]
%{year: year} = date
year # 22018
~D
is a sigil, sigils are shortcuts to create values
~w(chocolate jelly mint)
# ["chocolate", "jelly", "mint"], sigil for each word as string
date = ~D[2018-01-01]
%Date{day: day} = date
day # 1
%Date{day: day} = %{day: 1} # Match error
Matching functions
- function clauses must be collocated, nothing in between
defmodule NumberCompare do
def greater(number, other_number) do
check(number >= other_number, number, other_number)
end
defp check(true, number, _), do: number # matches if first arg true
defp check(false, _, other_number), do: other_number # matches if first arg is false
end
- multiple functions with different values (function clauses)
- select based on first arg match
- returns the number that is greater or equal
- Only
greater/2
is public,check/3
is internal, usingdefp
directive
Default function values
- only one default value for each parameter
- functions have fixed arity
- functions with same name but different number of parameters are different functions
\\
operator
defmodule Checkout do
def total_cost(price, quantity \\ 10), do: price * quantity
end
Checkout.total_cost(12) # 120, uses default value
Checkout.total_cost(12, 5) # 60, uses second arg
Guard Clauses
- using
when
keyword after functions parameters - Help us create better function signatures, reducing the need for function helpers
- Enforce which data we are expecting
defmodule NumberCompareV2 do
def greater(number, other_number) when number >= other_number, do: number
def greater(_, other_number), do: other_number
end
NumberCompareV2.greater(2,8) # 8
Uses a guard clause to return the number if greater, else the other
defmodule CheckoutV2 do
def total_cost(price, tax_rate) when price >= 0 and tax_rate >= 0 do
price * (tax_rate + 1)
end
end
CheckoutV2.total_cost(40, 0.1) # 44.0
CheckoutV2.total_cost(-2, 0.2) # FunctionClauseError
CheckoutV2.total_cost(42.3, "Hellow, World!") # ArithmeticError
- The last example passes the guard check because you can compare string to number and it's greater than 0.
- This makes it practical to sort mixed lists.
Anonymous function arguments
number_compare = fn
number, other_number when number >= other_number -> number
_, other_number -> other_number
end
number_compare_v3.(1,2) # 2
- Functions/Operators allowed in guard clause
- Can't use standard functions because the checker needs to be fast and have no side effects
- Erlang and Elixir ensure purity and speed by creating a list of authorized functions and that can be expanded using macro functions.
Macros
defmodule EvenOrOdd do
require Integer
def check(number) when Integer.is_even(number), do: "even"
def check(number) when Integer.is_odd(number), do: "odd"
end
EvenOrOdd.check(42) # "even"
EvenOrOdd.check(43) # "odd"
require
is needed to import macro functions- code is generated before evaluating
- require is lexically scoped
- create macro with
defguard
directive
defmodule CheckoutV2 do
defguard is_rate(value) when is_float(value) and value >= 0 and value <= 1
defguard is_cents(value) when is_integer(value) and value >= 0
def total_cost(price, tax_rate) when is_cents(price) and is_rate(tax_rate) do
price + tax_cost(price, tax_rate)
end
def tax_cost(price, tax_rate) when is_cents(price) and is_rate(tax_rate) do
price * tax_rate
end
end
CheckoutV2.tax_cost(40, 0.1) # 4.0
CheckoutV2.tax_cost(-2, 0.2) # FunctionClauseError
CheckoutV2.tax_cost(42.3, "Hello, World!") # FunctionClauseError
- creates a custom macro with defguard
Type specs/declaration
- dynamically types
- Don't have to be defensive about types
- The compiler never uses type specifications to optimize or modify the code.
- Use automated tests and pattern matching to ensure things are working
- Type specifications are useful for creating documentation and have static analysis to find inconsistencies and possible bugs
- Type Specs
- Dialyzer tool uses types specs to static check
- Dialyzer
dialyzer: [plt_add_apps: [:mix]]
@type t :: %DungeonCrawl.Character{
name: String.t(),
description: String.t(),
hit_points: non_neg_integer(),
max_hit_points: non_neg_integer(),
attack_description: String.t(),
damage_range: Range.t()
}
Bitwise
&&&
bitwise and
Recursion
- bounded recursion is a recursive function with an end
defmodule Sum do
def up_to(0), do: 0
def up_to(n), do: n + up_to(n - 1)
end
Sum.up_to(10) # 55
- The first clause matches if the arg is 0. else the second clause recurses.
- The bounded clause must be first to protect from infinite repetition
recurse a list
defmodule Math do
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
end
Math.sum([10,5,15]) # 30
Math.sum([]) # 0
Transforming Lists
[:a | [:b, :c]] # [:a, :b, :c]
[:a, :b | [:c]] # [:a, :b, :c]
[:a, :b, :c] # [:a, :b, :c]
- These will build a new list, one element at a time
- This syntax is prepending an element to a list, which is much faster than
++
defmodule EnchanterShop do
def test_data do
[
%{title: "Longsword", price: 50, magic: false},
%{title: "Healing Potion", price: 60, magic: true},
%{title: "Rope", price: 10, magic: false},
%{title: "Dragon's Spear", price: 100, magic: true}
]
end
@enchanter_name "Edwin"
def enchant_for_sale([]), do: []
def enchant_for_sale([item = %{magic: true} | incoming_items]) do
[item | enchant_for_sale(incoming_items)]
end
def enchant_for_sale([item | incoming_items]) do
new_item = %{title: "#{@enchanter_name}'s #{item.title}", price: item.price * 3, magic: true}
[new_item | enchant_for_sale(incoming_items)]
end
end
EnchanterShop.enchant_for_sale(EnchanterShop.test_data)
output
[
%{magic: true, price: 150, title: "Edwin's Longsword"},
%{magic: true, price: 60, title: "Healing Potion"},
%{magic: true, price: 30, title: "Edwin's Rope"},
%{magic: true, price: 100, title: "Dragon's Spear"}
]
Key-based Accessors
- keywords and maps have a syntax to access values using [], if the key is missing nil is returned and no error
- structs and maps can access with dot notation, if the key is missing an error is raised
item = %{magic: true, price: 150, title: "Edwin's Longsword"}
item[:title] # "Edwin's Longsword"
item[:owner] # nil
item[:creator][:city] # nil
item.title # "Edwin's Longsword"
item.owner # raises a KeyError
Recursion techniques
1.Decrease and conquer
defmodule Factorial do
def of(0), do: 1
def of(n) when n > 0, do: n * of(n - 1)
end
Factorial.of(5) # 120
2.Divide and conquer
defmodule Sort do
def ascending([]), do: []
def ascending([a]), do: [a]
def ascending(list) do
half_size = div(Enum.count(list), 2)
{list_a, list_b} = Enum.split(list, half_size)
merge(ascending(list_a), ascending(list_b))
end
defp merge([], list_b), do: list_b
defp merge(list_a, []), do: list_a
defp merge([head_a | tail_a], list_b = [head_b | tail_b]) when head_a <= head_b do
[head_a | merge(tail_a, list_b)]
end
defp merge(list_a = [head_a | _], [head_b | tail_b]) when head_a > head_b do
[head_b | merge(list_a, tail_b)]
end
end
Sort.ascending([9,5,1,5,4]) # [1, 4, 5, 5, 9]
Sort.ascending([2,2,3,1]) # [1, 2, 2, 3]
Tail-Call Optimization
- the compiler reduces functions in memory without allocating more memory
- to use, ensure the last expression of the function is a call to a function
- if the last expression is a function call, then the current functions return is he return of the new function call and it doesn't need to keep the current in memory
defmodule TrFactorial do
def of(n), do: factorial_of(n, 1)
defp factorial_of(0, acc), do: acc
defp factorial_of(n, acc) when n > 0, do: factorial_of(n - 1, n * acc)
end
TrFactorial.of(10000) # returns much faster than the original body recursive
unbounded recursion
- can't predict the number of repetitions
defmodule Navigator do
def navigate(dir) do
expanded_dir = Path.expand(dir)
go_through([expanded_dir])
end
defp go_through([]), do: nil
defp go_through([content | rest]) do
print_and_navigate(content, File.dir?(content))
go_through(rest)
end
defp print_and_navigate(_dir, false), do: nil
defp print_and_navigate(dir, true) do
IO.puts(dir)
children_dirs = File.ls!(dir)
go_through(expand_dirs(children_dirs, dir))
end
defp expand_dirs([], _relative_to), do: []
defp expand_dirs([dir | dirs], relative_to) do
expanded_dir = Path.expand(dir, relative_to)
[expanded_dir | expand_dirs(dirs, relative_to)]
end
end
Navigator.navigate("..") # prints dir
adding bounds
defmodule DepthNavigator do
@max_depth 2
def navigate(dir) do
expanded_dir = Path.expand(dir)
go_through([expanded_dir], 0)
end
defp go_through([], _current_depth), do: nil
defp go_through(_dirs, current_depth) when current_depth >= @max_depth, do: nil
defp go_through([content | rest], current_depth) do
print_and_navigate(content, File.dir?(content), current_depth)
go_through(rest, current_depth)
end
defp print_and_navigate(_dir, false, _current_depth), do: nil
defp print_and_navigate(dir, true, current_depth) do
IO.puts(dir)
children_dirs = File.ls!(dir)
go_through(expand_dirs(children_dirs, dir), current_depth + 1)
end
defp expand_dirs([], _relative_to), do: []
defp expand_dirs([dir | dirs], relative_to) do
expanded_dir = Path.expand(dir, relative_to)
[expanded_dir | expand_dirs(dirs, relative_to)]
end
end
DepthNavigator.navigate("..") # bounded by depth
recurse anonymous functions
fact_gen = fn me ->
fn
0 -> 1
x when x > 0 -> x * me.(me).(x - 1)
end
end
factorial = fact_gen.(fact_gen)
factorial.(5)
me
argument represents the factorial generator, representing itselfme.(me)
produces a factorial function- not very expressive code, not straight forward but possible
c("factorial.ex")
factorial = &Factorial.of/1
factorial.(5)
- use the capturing operator to reference a function
- using named functions as values
- easier to read than recursive anonymous function
Higher Order Functions
- pass a function into a function
- hides complexity
defmodule MyListV2 do
def enchanted_items do
[
%{title: "Edwin's Longsword", price: 150},
%{title: "Healing Potion", price: 60},
%{title: "Edwin's Rope", price: 30},
%{title: "Dragon's Spear", price: 100}
]
end
def each([], _function), do: nil
def each([head | tail], function) do
function.(head)
each(tail, function)
end
end
MyListV2.each(MyListV2.enchanted_items, fn item -> IO.puts item.title end)
increase_price = fn i -> %{title: i.title, price: i.price * 1.1} end
increase_price = fn item -> update_in(item.price, &(&1 * 1.1)) end # same as above using built-in higher-order function
def reduce([], acc, _function), do: acc
def reduce([head | tail], acc, function) do
reduce(tail, function.(head, acc), function)
end
sum_price = fn item, sum -> item.price + sum end
MyListV2.reduce(MyListV2.enchanted_items, 0, sum_price) # 340
def filter([], _function), do: []
def filter([head | tail], function) do
if function.(head) do
[head | filter(tail, function)]
else
filter(tail, function)
end
end
MyListV2.filter(MyListV2.enchanted_items, fn item -> item.price < 70 end)
MyListV2.filter(["a", "b", "c", "d"], &(&1 > "b")) # ["c", "d"]
Using the enum module
- The Enum module contains each, map, reduce and filter list operations.
- Work with any data type that respects Enumerable Protocol
Enum.each(["dogs", "cats", "flowers"], &(IO.puts String.upcase(&1)))
Enum.map(["dogs", "cats", "flowers"], &String.capitalize/1)
Enum.reduce([10,5,5,10], 0, &+/2)
Enum.filter(["a", "b", "c", "d"], &(&1 > "b"))
medals = [
%{medal: :gold, player: "Anna"},
%{medal: :silver, player: "Joe"},
%{medal: :gold, player: "Zoe"},
%{medal: :bronze, player: "Anna"},
%{medal: :silver, player: "Anderson"},
%{medal: :silver, player: "Peter"}
]
Enum.group_by(medals, &(&1.medal), &(&1.player))
# %{bronze: ["Anna"], gold: ["Anna", "Zoe"], silver: ["Joe", "Anderson", "Peter"]}
Comprehensions
- generator function that will assign each item to the list of variable a
for a <- ["dogs", "cats", "flowers"], do: String.upcase(a)
for a <- ["Willy", "Anna"], b <- ["Math", "English"], do: {a,b} # combining
parseds = for i <- ["10", "hot dogs", "20"], do: Integer.parse(i) # [{10, ""}, :error, {20, ""}]
for {n,_} <- parseds, do: n # [10,20]
for n <- [1, 2, 3, 4, 5, 6, 7], n > 3, do: n # [4, 5, 6, 7]
Pipelining your functions
defmodule HigherOrderFunctions do
def compose(f, g) do
fn arg -> f.(g.(arg)) end
end
end
import HigherOrderFunctions
first_letter_and_upcase = compose(&String.upcase/1, &String.first/1)
first_letter_and_upcase.("works") # W
# Using pipe and capture operators
first_letter_and_upcase = &(&1 |> String.first |> String.upcase)
first_letter_and_upcase.("works") # W
defmodule MyString do
def capitalize_words(title) do
words = String.split(title)
capitalize_words = Enum.map(words, &String.capitalize/1)
Enum.join(capitalize_words, " ")
end
end
# elixir way
defmodule MyString do
def capitalize_words(title) do
title
|> String.split
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
end
end
# with smaller functions
defmodule MyString do
def capitalize_words(title) do
title
|> String.split()
|> capitalize_all
|> join_with_whitespace
end
def capitalize_all(words) do
Enum.map(words, &String.capitalize/1)
end
def join_with_whitespace(words) do
Enum.join(words, " ")
end
end
MyString.capitalize_words("a whole new world") # "A Whole New World"
Partial Application
defmodule WordBuilder do
def build(alphabet, positions) do
partial = fn at -> String.at(alphabet, at) end
letters = Enum.map(positions, partial)
Enum.join(letters)
end
end
# refactored with function capturing
defmodule WordBuilder do
def build(alphabet, positions) do
letters = Enum.map(positions, &String.at(alphabet, &1))
Enum.join(letters)
end
end
WordBuilder.build("world", [4, 1, 1, 2]) # "door"
Infinite data
- streams have no end
range = 1..10
Enum.each(range, &IO.puts/1) # prints each
defmodule FactorialV2 do
def of(0), do: 1
def of(n) when n > 0 do
1..10_000_000
|> Enum.take(n)
|> Enum.reduce(&(&1 * &2))
end
end
defmodule FactorialV2 do
def of(0), do: 1
def of(n) when n > 0 do
Stream.iterate(1, &(&1 + 1))
|> Enum.take(n)
|> Enum.reduce(&(&1 * &2))
end
end
FactorialV2.of(10000)
integers = Stream.iterate(1, fn previous -> previous + 1 end)
Enum.take(integers, 5)
defmodule Halloween do
def give_candy(kids) do
~w(chocolate jelly mint)
|> Stream.cycle()
|> Enum.zip(kids)
end
end
Halloween.give_candy(~w(Mika Anna Ted Mary Alex Emma))
Eager/Lazy
# slow, eager
defmodule ScrewsFactory do
def run(pieces) do
pieces
|> Enum.map(&add_thread/1)
|> Enum.map(&add_head/1)
|> Enum.each(&output/1)
end
defp add_thread(piece) do
Process.sleep(50)
piece <> "--"
end
defp add_head(piece) do
Process.sleep(100)
"o" <> piece
end
defp output(screw) do
IO.inspect(screw)
end
end
metal_pieces = Enum.take(Stream.cycle(["-"]), 100)
ScrewsFactory.run(metal_pieces)
# lazy
defmodule ScrewsFactory do
def run(pieces) do
pieces
|> Stream.map(&add_thread/1)
|> Stream.map(&add_head/1)
|> Enum.each(&output/1)
end
defp add_thread(piece) do
Process.sleep(50)
piece <> "--"
end
defp add_head(piece) do
Process.sleep(100)
"o" <> piece
end
defp output(screw) do
IO.inspect(screw)
end
end
# very fast stream
defmodule ScrewsFactory do
def run(pieces) do
pieces
|> Stream.chunk(50)
|> Stream.flat_map(&add_thread/1)
|> Stream.chunk(100)
|> Stream.flat_map(&add_head/1)
|> Enum.each(&output/1)
end
defp add_thread(pieces) do
Process.sleep(50)
Enum.map(pieces, &(&1 <> "--"))
end
defp add_head(pieces) do
Process.sleep(100)
Enum.map(pieces, &("o" <> &1))
end
defp output(screw) do
IO.inspect(screw)
end
end
- creating chunks and flat_map creates a queue before processing
Application
- mix is a built in CLI for generating a project structure
mix new app_name
use ExUnit.Case
adds new capabilities to the current module, mostly with metaprogramming
defmodule DungeonCrawl do
@moduledoc """
Documentation for `DungeonCrawl`.
"""
@doc """
Hello world.
## Examples
iex> DungeonCrawl.hello()
:world
"""
def hello do
:world
end
end
- This example is run as test, to ensure the doc examples work
- Exunit Docs
mix new
and mix test
are tasks. You can add custom tasks
defmodule Mix.Tasks.Start do
use Mix.Task
def run(_), do: IO.puts("Hello, World!")
end
mix start # Hello, World!
iex -S mix # load the module
alias DungeonCrawl.Character
allows to reference %Character
Protocol
- create a single interface that works on different data types
- polymorphic
- protocol doc
- if you own the struct, put the implementation in the same file as the struct
- if you don't own the struct, but own the protocol, put the implementation inside the protocol file
- if you own neither the struct or the protocol, create a file, with the protocol name and put the implementation there
- great for structs but not for simple modules
defprotocol DungeonCrawl.Display do
def info(value)
end
defimpl DungeonCrawl.Display, for: DungeonCrawl.Room.Action do
def info(action), do: action.label
end
defimpl DungeonCrawl.Display, for: DungeonCrawl.Character do
def info(character), do: character.name
end
Module Behaviors
- a behavior is a contract between a module and the client code tht is using it
- common interface across multiple modules
defmodule DungeonCrawl.Room.Trigger do
@callback run(character :: any, action :: any) :: any
end
defmodule DungeonCrawl.Room.Triggers.Exit do
@behaviour DungeonCrawl.Room.Trigger
def run(character, _), do: {character, :exit}
end
Protocols vs Behaviors Protocols work with structs, and behaviors work with modules. Protocols create a function interface to work with several data types. Behaviors define a list of functions that a module should implement.
Control flow of impure functions
- First strategy is pattern matching.
- Use case, if or function clauses to handle impure function results
example control flow with matching
defmodule Other.Shop do
def checkout() do
quantity = ask_number("Quantity?")
price = ask_number("Price?")
calculate(quantity, price)
end
def calculate(:error, _), do: IO.puts("Quantity is not a number")
def calculate(_, :error), do: IO.puts("Price is not a number")
def calculate({quantity, _}, {price, _}), do: quantity * price
def ask_number(message) do
(message <> "\n")
|> IO.gets()
|> Integer.parse()
end
end
case
def ask_for_index(options) do
answer =
options
|> display_options()
|> generate_question()
|> Shell.prompt()
|> Integer.parse()
case answer do
:error ->
display_invalid_option()
ask_for_index(options)
{option, _} ->
option - 1
end
end
try/catch/raise/rescue
- try wraps a code block, if an error is raised you can use rescue to recover
- you can capture values in catch
- throwing values or raising errors is unusual in FP
- You can identify functions that raise errors because the name has
!
- MatchError is too generic to rescue
- Exception Doc
- elixir devs prefer raise/rescue due to lack of clarity and increased complexity
The difference between throw/catch and raise/rescue is that try/catch doesn't necessarily mean an error. It will stop the function from throwing a value that must be caught, like control-flow structures.
raise/rescue
def ask_for_option(options) do
try do
options
|> display_options()
|> generate_question()
|> Shell.prompt()
|> parse_answer!()
|> find_option_by_index!(options)
rescue
e in DungeonCrawl.CLI.InvalidOption ->
display_error(e)
ask_for_option(options)
end
end
try/catch
def ask_for_option(options) do
try do
options
|> display_options()
|> generate_question()
|> Shell.prompt()
|> parse_answer!()
|> find_option_by_index!(options)
catch
{:error, message} ->
display_error(message)
ask_for_option(options)
end
end
# if you only need one try block you can omit try do
def ask_for_option(options) do
options
|> display_options()
|> generate_question()
|> Shell.prompt()
|> parse_answer!()
|> find_option_by_index!(options)
catch
{:error, message} ->
display_error(message)
ask_for_option(options)
end
end
Handling impure functions with the Error Monad
- Use when you have many functions in sequence and some can fail
- monad wraps a value with properties to give more information about that value, i.e. context
- Error monad has automatic skipping of function executions if the value has an error to handle in a central point
- To make it work, you need a bind function, bind knows how to combine the function and value
- Many libraries available: monad, towel, witchcraft and MonadEx
use Monad.Operators
import Monad.Result
success(42) ~>> (& &1 + 1) ~>> (& &1 + 2) # 45
error("wrong") ~>> (& &1 + 1) ~>> (& &1 + 2) # Monad.Result{type: :error, value: nil, error: "wrong"}
~>>
is the bind operator, left side expects a monad and the right expects a function. It executes values in success and skips in error context.
def ask_for_option(options) do
result =
return(options)
~>> (&display_options/1)
~>> (&generate_question/1)
~>> (&Shell.prompt/1)
~>> (&parse_answer/1)
~>> (&find_option_by_index(&1, options))
if success?(result) do
result.value
else
display_error(result.error)
ask_for_option(options)
end
end
With
- combine multiple match clauses
- if all the clauses match run do, else code stops and return non-matching
- you should use with if you have function pipelines that can result in an errors
# before
def checkout() do
try do
{quantity, _} = ask_number("Quantity?")
{price, _} = ask_number("Price?")
calculate(quantity, price)
rescue
MatchError -> "It's not a number"
end
end
def checkout() do
result = with {quantity, _} <- ask_number("Quantity?"), {price, _} <- ask_number("Price?") do
calculate(quantity, price)
end
if result == :error, do: IO.puts("It's not a number"), else: result
end
# alternative with else
def checkout() do
with {quantity, _} <- ask_number("Quantity?"), {price, _} <- ask_number("Price?") do
calculate(quantity, price)
else
:error -> IO.puts("It's not a number")
end
end
<-
will execute block on the right side and pattern match the left