- Learn Functional Programming with Elixir
- Running Code
- Chapter 1 - Thinking Functionally
- Chapter 2 - Working with Variables and Functions
- Chapter 3 - Using Pattern Matching to Control the Program Flow
- Chapter 4 - Diving into Recursion
- Chapter 5 - Using Higher-Order Functions
- Chapter 6 - Designing Your Elixir Applications
Table of contents generated with markdown-toc
-
Get Elixir version
$ elixir -v -
Open Elixir's interactive shell
$ iex
(Press Ctrl + C twice to exit) -
Execute script file
$ elixir hello_world.exs -
File Extensions
.ex for compiled files
.exs for script files
The rules that govern typical everyday programming are changing. That doesn't happen often. When it does, something important is going on.
We need to write code that takes advantage of concurrency and parallelism.
Imperative Languages have shared mutating values, which can be dangerous for concorrency and easily introduce bugs.
Instead of libraries that offer mechanisms to help lock and sync the changes, functional programming offers a better alternative.
In FP, functions are the basic building blocks, values are immutable, and the code is declarative.
In FP, all values you create are immutable, no matter the operation we apply to it:
list = [1, 2, 3, 4]
# => [1, 2, 3, 4]
List.delete_at(list, -1)
# => [4]
list ++ [1]
# => [1, 2, 3, 4, 1]
IO.inspect list
# => [1, 2, 3, 4]- Functions are the primary tools for building a program
- They are usually short and expressive
- We combine multiple little functions to create a larger program
- Values are passed explicitly between functions
- Functions can be arguments and results
- Values are immutable
- The result is affected only by the function's arguments
- No side effects beyond the value returned
add2 = fn (n) -> n + 2 end
add2.(2)
# => 4More complex, unpredictable results and with side effects (Chapter 7)
Elixir's focus is on the data transformation flow, and the pipe ( |> ) operator can be used to combine multiples functions' calls and results, where the result of each expression will be passed as a value to the next function.
title
|> String.split
|> capitalize_all
|> join_with_whitespace- Generates less and simpler code
- Less code = fewer bugs
- what's necessary to solve a problem instead of how to solve a problem( imperative)
- Values are anything that can represent data: Numbers, Strings, Fuctions, Maps and etc..
- Operators compute values and generate a result: +, *, -...
- Literals represent values that humans can easily understand.
- Elixir generates result for any expression:
iex> a = 2
2- It's not possible to add text and a number:
iex> "2" + 2
(ArithmeticError) ...| Type | Useful for | Examples |
|---|---|---|
| string | Text | "Hello, World" |
| integer | Numbers | 10 20 -2 |
| float | Real numbers | 10.8 -1.45 |
| boolean | Logical operators | true, false |
| atom | Identifiers | :ok, :error, :exit |
| tuple | Building collections of known sizes | {:ok, "Hello"} {1,2,3} |
| list | Building collections of unknown sizes | [1,2] ["a", "b"] |
| map | Looking up a value in a dictionary by key | %{id: 123, name: "Anna"} %{12 => "User"} |
| nil | NULL | nil |
| Operator | Useful for | Examples |
|---|---|---|
| + | Adding numbers | 1 + 2, 1.5 + 2 |
| - | Substracting numbers | 1-2, 1.5-2 |
| / | Dividing numbers | 10/2 |
| * | Multipling numbers | 10*2.5 |
| == | Checking if equal | 1 == 1.0 |
| != | Checking if not equal | "1"!=1 |
| < | Less than | 1<2 |
| > | Greater than | 2>1 |
| ++ | Concatenating two lists | [1,2] ++ [3,4] |
| <> | Concatenating tow strings or binaries | "Hello, " <> "World!" |
| "#{}" | String interpolation | "Hello, #{name}!" |
- and, or, not are made to work with Boolean values,
iex> true and true
true
iex> true or false
true
iex> not true
false
iex> 1 and true
(BadBooleanError)
iex> true and 1
true
(right side part won't be computed)- &&, ||, ! accept falsy(nil and false) or truthy(anything else) values and return a value based on the operator we use
iex> nil && 1
nil
iex> true && "Hello, World!"
"Hello, World!"
iex> nil || 1
1
iex> !true
false- Explicit names
- snake_case format
iex> quantity = 10
10
iex> product_price = 11.5
11.5
iex> total_cost = product_price * quantity
115.0- Functions receive input, computes, and outputs the last expression
- Good practice to keep param number below 5
- Must be bound to a variable to be reused
- Also known as lambdas
- func = fn param_1, p_2 -> body end
- func.("param", 2)
- func_2 = fn -> body end
- func_2.()
- func_3 = &(&1 * &2)
- func_3.(10, 2)
- func_4 = & &1 * &2
- func_4.(10, 2)
iex> hello = fn name -> "Hello, " <> name <> "!" end
iex> hello.("Ana")
"Hello, Ana!"- Functions are values of type function
iex> flat_fee = fn _ -> 5 end
iex> full_price = fn price, fee -> price + fee.(price) end
iex> full_price.(10, flat_fee)
15- Use closures, as they have access to var values both inside and outside of the code block
- Closures "remember" all the free vars from when they were created
- Scope is a part of a program (code block)
- Lexical scope is related to the visibility of the vars where they were defined
iex> answer = 42
iex> make_answer = fn -> other_answer = 88 + answer end
iex> make_answer.()
50
iex> other_answer
*** (Error)
iex> answer = 0
iex> make_answer.()
50- named functions are defined inside modules
- atom or aliases can be used to name a function
- Alias is any word that starts with a capital letter ( String )
- During compile time, aliases become atoms (String == :"Elixir.String" )
- To call a function inside a module, use Module.function()
- It's possible to omit the parenthesis when calling a function (
IO.puts "Hello")
- Full Docs
- Kernel module doesn't need the module name when called
div(1,2) - All other modules do
String.capitalize("HellO THerE")
- use snake_case.ex for module file
- use lib/namespace/module.ex for module path
- use CamelCase, Namespace.Module for module name
- only one module per file
- use snake_case for function name
- one line functions can be defined as
def func_name(), do: expression
defmodule Ecommerce.Checkout do
def total_cost(price, tax_rate) do
price * (tax_rate + 1)
end
def total_cost_single(price, tax_rate), do: price * tax_rate + 1
end- to compile and use a file inside iex, use:
iex> c("lib/ecommerce/checkout.ex")
iex> Ecommerce.Checkout.total_cost(100, 0.2)
120.0- modules attributes can be defined with @ and are used as annotations, temporary storage, or constants
@file_name "task_list.md" - use import directive to import modules' functions
import File, only: [write: 3, read: 1] - import directive should only be used when function names can't cause confusion
- use the &function/arity operator to bind a named function to a variable
iex> upcase = &String.upcase/1
iex> upcase.("oi")
OIElixir's pattern match shapes everything you program.
iex> 1 = 1
1
iex> 2 = 1
** (MatchError)
iex> x = 1 (bind and match)
1
iex> 1 = x
1
iex> 2 = x
** (MatchError)
iex> x = 2 (rebind and match)
2
iex> ^x = 2 (pin operator avoids rebind and uses variable value)
2
iex> ^x = 1 (avoids rebind and uses variable value)
** (MatchError)- You can use the _ wildcard to ignore parts of a Pattern Matching
Pattern matching is useful for extracting parts of values to variables in a process called destructing.
iex> "Authentication: " <> credentials = "Authentication: Basic dxYz123"
"Authentication: Basic dxYz123"
iex> credentials
"Basic dxYz123"- It's not possible to use a variable on the left side of the <> operator
iex> first_name <> "Doe" = "John Doe"
** (CompileError)Tuples are often used to pass a signal with values
- We can match and bind multiple items of a tuple
iex> {a, b, c} = {4, 5, 6}
{4, 5, 6}
iex> b
5- Tuples are useful for signaling successess and failures in a function's return
iex> my_function = fn -> {:ok, 42} end
iex> {:ok, answer} = my_function.()
iex> answer
42
iex> {:error, reason} = my_function.()
** (MatchError)- In Elixir, lists are linked lists
iex> [a, a, a] = [1, 1, 1]
[1, 1, 1]
iex> [a, a, a] = [1, 2, 1]
** (MatchError)
iex> [a, b, a] = [1, 2, 1]
[1, 2, 1]
iex> [a, b, 1] = [1, 2, 1]
[1, 2, 1]
iex> [_, b, _] = [1, 2, 1]
[1, 2, 1]-Using the | operator to split arrays
iex> [head | tail] = [1,2,3,4]
iex> head
1
iex> tail
[2,3,4]iex> [a, b | tail] = [1,2,3,4]
iex> a
1
iex> b
1
iex> tail
[3,4]iex> [head | tail] = [1]
iex> head
1
iex> tail
[]iex> [head | tail] = []
** MatchErrorMaps are data types structured in key/value pairs
iex> user_signup = %{email: "johndoe@email.com", password: "1234"}The key is a atom, if you need to use other values as key you should use =>
iex> sales = %{"2017/01"=> 200, "2017/02"=> 250}It's possible to create nested structures
%{
name: "John Doe",
programming_languages: ["Ruby", "Python", "Java"],
location: %{city: "New York", country:"US"}
}- Pattern Match and bind to variable
iex> abilities = %{strength: 16, intelligence: 10}
iex> %{strength: s_value} = abilities
iex> s_value
16
iex> %{wisdom: w_value} = abilities
** MatchError
iex> %{strength: 16, intelligence: i_value} = abilities
iex> i_value
10
iex> %{strength: s_value = 16} = abilities (check and bind)
iex> s_value
16
iex> strength_value = 16
iex> %{strength: ^strength_value} = abilities (use value of ^strength_value)- Keyword List is a list of two-element tuples
- It allows duplicate dkeys
- Keys must be atoms
iex> [b, c] = [a: 1, a: 12]
iex> b
{:a, 1}x = %{a: 1, a:12} # result in {a: 12}
x = [a: 1, a: 12] # OK
x = [{:a, 1}, {:a, 12}] # same from above
x = %{1=> :a, 2 => :b} # OK
x = [1 => :a, 2 => :b] # Syntax error- The syntax is similar, but their limitations make them handy for different cases
- Structs are extensions of mapping structures
- Useful for representing consistent structures
- Similar to a class with props
- Pattern Matching works like it does with Maps
- ~D is a date sigil
- The name of the struct can be used to match only the struct
iex> date = ~D[2018-01-01]
iex> %{year: year} = date
iex> year
2018
iex> date = ~D[2018-01-01]
iex> %Date{year: year} = date
iex> year
2018
iex> date = %{year: 2018}
iex> %Date{year: year} = date
** MatchError- Sigils are shortcuts to create values with a simplified text representation
iex> ~D[2018-01-01]
~D[2018-01-01]
iex> ~w(chocoalte jelly mint)
["chocolate", "jelly", "mint"]- Pattern Matching and Functions are the fundamental tools we use to control the program flow see number_compare.ex
- Arguments from functions are pattern-matching expressions
- To define a private function inside a module, use defp
- Use \\ to apply a default value to a named function
- Elixir will create multiple functions automatically on compile time
- In Elixir, functions have fixed arity
- Arity is part of the function's unique name
defmodule Checkout do
# &total_cost/1 and &total_cost/2 are created
def total_cost(price, quantity \\ 10), do: price * quantity
end
iex> Checkout.total_cost(12)
120
iex> Checkout.total_cost(12, 5)
60- They permit us to add Boolean expression in our functions
- Help create better function signatures
- Can be created by using the when keyword after params
- Possible to use guards on named and annonymous functions
def greater(number, other_number) when number >= other_number, do: number
def greater(_, other_number), do: other_number
var_greater = fn
number, other_number when number >= other_number -> number
_, other_number -> other_number
end- Macro functions can be easily created with the defguard directive
- When macro is used, we need to use the directive require
- Function clauses are usually used to control the flow of the program
- Elixir built-in control-flow structures like case, cond, if and unless can also be used
- Useful when we want to check an expression with multiple pattern matching clauses
- Useful with functions that may have unexpected effect
- It's possible to use guards on the clauses
- When a clause is matched, it'll execute the code and return the last expression
- if no clause is matched, an error is raised
case Boolean expression do
{:ok, value} -> value
:error -> IO.puts("Error")
end- Useful we want to check variables in logical expressions
- Structure similar to case
- When a condition is truthy, execute related code
- if no condition is truthy, raise error
result = cond do
age < 12 -> "kid"
age <= 18 -> "Teen"
- Useful when you want to execute a command based on a truthy expression
- unless can be used as if not
- else block is optional, if not given, return nil
def greater_if(number, other_number) do
if number >= other_number do
number
else
other_number
end
end
def greater_unless(number, other_number) do
unless number < other_number do
number
else
other_number
end
end
result = if(number >= other_number, do: number, else: other_number)- While we use for and while loops on imperative languages, we have recursion in FP
- A bounded recursion have an end
- Most common type
- Number of iterations is directly associated with the arguments
- Good practice: Declare the boundary clause first
defmodule Sum do
def up_to(0), do: 0
def up_to(n), do: n + up_to(n -1)
end
iex> Sum.up_to(3) #will call the function n+1 times
6- Use syntax [head | tail] to navigate through lists using recursion
- Can iterate through any data structure
defmodule Math do
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
end
iex> Math.sum([5,2,3])
10
iex> Math.sum([])
0- Transforming lists requires repetitive steps
- [head | tail] is useful for destructuring and constructing new lists
- [|] to prepend is faster than ++
iex> [:a | [:b, :c]]
[:a, :b, :c]
iex> [[:b, :c] | :a ]
[:b, :c, :a]
iex> [:a, :b, :c]
[:a, :b, :c]iex> item = %{a: 1, b: 2}
iex> item[:a]
1
iex> item.a
1
iex> item[:c]
nil
iex> item.c
** KeyError- Reduce a problem to its simplest form and start solving it incrementally
- E.g: Factorial
defmodule Factorial do
def of(0), do: 1
def of(n) when n > 0, do: n * of(n-1)
end- Separate the problem into two or more parts
- Process independly and combine in the end
- E.g: Sorting functions
defmodule Sort do
def asc([]), do: []
def asc([a]), do: [a]
def asc(list) do
half_size = div(Enum.count(list), 2)
{list_a, list_b} = Enum.split(list, half_size)
merge(
asc(list_a),
asc(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 | _]) 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- Compiler reduces functions in memory without allocating more memory
- Last expression of a function must be a function call
- Common approach is to replace the use of the function result with an extra argument
- Extra argument accumulates the results of each iteration
- body-recursive functions are simpler to read and maintain, so they should be used when a low number of recursion is expected
- tail-recursive functions should be used when a large number of iterations is expected
Compare:
defmodule Factorial do
def of(0), do: 1
def of(n) when n > 0, do: n * of(n -1) # will create stack
end
defmodule TailCallFactorial do
def of(n), do: fac_of(n, 1)
defp fac_of(0, acc), do: acc
defp fac_of(n, acc) when > 0, do: fac_of(n - 1, n * acc) # will use tail-recursive optmization
end- Unbounded recursion is when we can't predict the number of repetition for a recursive function
- For example: web crawler, directory navigator
- Adding Boundaries
- Avoiding Infinite Loops
- Recursion with anonymous functions isn't straightforward and should avoided
- It's necessary to wrap the function in another function to avoid nonexistent error
iex> fact_gen = fn me ->
fn
0 -> 1
x when x > 0 -> x * me.(me).(x - 1)
end
end
iex> factorial = fact_gen.(fact_gen)
iex> factorial.(5)- Higher-Order functions are those that take functions as arguments and/or return them.
- Useful for hiding complexity and laborius functions
- Receive functions as arguments as your receive any other arg
def each([], function), do: nil - Use functions as you use function values
function.() - It's possible to send annonymous functions
masMyList.each([], fn item -> IO.puts item end) - Value functions
MyList.each([], my_function) - Named functions
MyList.each([], &String.capitalize/1) - Operators as named functions
MyList.reduce([], 0, &+/2) - Operators as named functions
Enum.sort([], &<=/2)
- Works with maps, lists, tuples and any type that implements the Enumerable protocol
- Enum.each
- Enum.map
- Enum.reduce
- Enum.filter
- Enum.count
- Enum.sort
- Enum.sum
- Enum.uniq
- Enum.sort
- Enum.join
- using for, enumerables can be easily iterated, mapped and filtered
iex> for a <- ["dogs", "cats", "flowers"], do: String.upcase(a)
["DOGS", "CATS", "FLOWERS"]- each item of the list will be assigned to a
- it's possible to have more than one generator
iex> for a <- ["Willy", "Anna"], b <- ["Math", "English"], do: {a, b}
[{"Willy", "Math"}, {"Willy", "English"}, {"Anna", "Math"}, {"Anna", "English"}]- we can filter by using pattern matching
iex> parseds = for i <- ["10", "hot dogs", "20"], do: Integer.parse(i)
[{10, ""}, :error, {20, ""}]
iex> for {n, _} <- parseds, do: n
[10, 20]- we can also filter with an expression for truthy values
iex> for n <- [1,2,3,4,5], n > 3, do: n
[4,5]- Used to execute many functions in sequence
- Elixir doesn't have a built-in compose function like other functional languages, the alternative is to use pipes
iex> first_letter_and_upcase = &(&1 |> String.first |> String.upcase)
iex> first_letter_and_upcase.("works")
"W"- The result of each pipe expression will be passed as the first argument of the next pipe
- The result of the pipeline is the result of the last pipe
- Pipes should be used for 2+ expressions for better readability
- Lazy operations provide alternative techniques of programming and creating efficient programs
- Other FP languages have currying, Elixir has partial application
- Partial application is used to postopone a function's execution
- The function is wrapped in another function with a fixed value of any of the arguments
- It's common to use the function-capturing-syntax
defmodule WordBuilder do
def build(alphabet, positions) do
letters = Enum.map(positions, &(String.at(alphabet),&1))
Enum.join(letters)
end
end- In programming, infinite is something that's always expanding
- E.g. Web Server waiting for calls, messaging broker handling events
- streams type represents a flow of data that may not have an end
- Stream module contains many functions to create and operate streams
- Range literal is a stream
iex> range = 1..10 - It's a lazy collection, which are evaluated only when necessary
iex> Enum.each(range, &IO.puts/1)
- We can represent a collection that is always expanding using Stream.iterate/2
- The items of the stream will be evaluated dynamically, every time something asks for an item
- Needs a starting value and an increment function
iex> i = Stream.iterate(1, fn prev -> prev + 1 end)`
iex> Enum.take(i, 5)
[1,2,3,4,5]defmodule Factorial 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- Stream.cycle lets you create a list that will keep iterating through its items forever
defmodule Halloween do
def give_candy(kids) do
~w(chocolate jelly mint)
|> Stream.cycle
|> Enum.zip(kids)
end
end
iex> Halloween.give_candy(~w(Raff Jorge Anna Elza))
[{"chocolate", "Raff"}, {"jelly", "Jorge"}, {"jelly", "Anna"}, {"chocolate", "Elza"} ....]- Combine Elixir pipe and strems
- Can be done in two ways: eager or lazy
- Eager: each computation processes all items and send to next computation
- Lazy: each computation processes a chunk of items and send partial results
- Command-line interface (CLI)
- Provides essentials for developing any Elixir application
- Helps creates and maintain projects (compile, debug, test, manage dependencies)
- built-in Elixir
- mix new creates the initial structure to code your application
mix new project_name- mix test will run all the tests on test folder
- iex -S mix innitiates IEx with project compiled
- Mix tasks are mix commands like mix new and mix test
- It's possible to create our own tasks
- Must be inside lib/mix/tasks
- Structs are used to express our application domains
- We should define them on lib\namespace\module.ex
- Module name should be Namespace.Module
- Define struct using defstruct directive
- Use attribute_name: default_value for each prop
- alias directive can be used to create module shortcuts
alias Mix.Shell.IO, as: Shell
- protocol lets you create an interface that many data types can implement
- interfaces leads to better codebase design
- define protocol with defprotocol
defprotocol Protocol do
def function(param)
end- implement with defimpl
defimpl Protocol, for: ModuleThatImplements do
def function(param), do: param
end- if inside module, for is not needed
def Module do
defimpl Protocol do
def fuction(param), do: param
end
endConventions
- if you own the struct, put the implementation in the same file as the struct
- if you own the protocol and not the struct, put the implementation with the protocol
- if you don't own anything, create a file with the protocol name and put the implementation there
- behavior is a contract between a module and the client code
- Mix.Task is a behaviour
- @callback directive to define a function rule
@callback run(param1 :: any, param2 :: any) :: any- @behaviour directive to say module follows a behaviour
@behaviour Namespace.CallbackModule- Notations that say what your functions expect and return
- Good for static check and documentation
- use
@type tdirective inside the module
@type t :: %ModuleStruct{
prop: Type.t
}- Use Dialyzer to statically analyze the code
Impure Functions are those that can return different values from the same input.
The main strategy for creating a healthy codebase is to identify and isolate the parts that can have unexpected results, and make them predictable.
We'll discuss 4 strategies for this:
- Conditional structures: case, if, etc...
- Exception handling: try
- Error monads
- Elixir's with: pattern maching + conditional
- You shouldn't think pure is good and impure is evil, both are important to write an application
- It's good practice to build more pure functions and isolating impure parts with proper handling
- Always return consistent output when the same input is given, and no side effects are produced
- They can produce errors, but those are predictable
- May not return consistent output with the same input
- May produce side effects
- References values that aren't in the function's args
- Interact with content outside of the program context (File IO, database, API, logs, user input, etc...)
- If a functions calls an impure function, it becomes impure
- Throwing values and/or raising errors is unusual in FP
- Names from functions that raise errors end with !
- use defexception to define a exception struct
- use try rescue to catch exceptions
- throw exceptions with raise Exception
def checkout do
try do
{qty, _} = ask_number("Quantity?")
{price, _} = ask_number("Price?")
qty * price
rescue
MatchError -> "It's not a number"
end
end- Similar to raise and rescue
- Doesn't mean error, it's possible to throw variables
throw {:error, "Invalid option"}def ask_for_option(options) do
try do
options
|> display_options
|> parse_answer
catch {:error, message} ->
display_error(message)
end
end- Error Monad helps combining functions that can result in an error
- Clear sequence
- Error handling at a unique point
- A monad wraps a value with properties that give more information about the value (context)
- Makes it possible to combine functions with values to make automatic decisions
- Are not Elixir native, you must download a package from the internet
- Makes it possible to combine multiple matching clauses
- Should be used when there's a function pipeline that can result in an error
def checkout() do
result =
with {qty, _} <- ask_number("Qty?")
{price, _} <- ask_number("Price?"),
do: qty * price
if result == :error, do: IO.puts("It's not a number"), else: result
end