Skip to content

yangknet/LearnFunctionalProgrammingWithElixir-2

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Learn Functional Programming with Elixir

Table of contents generated with markdown-toc

Running Code

  • 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

Chapter 1 - Thinking Functionally

The rules that govern typical everyday programming are changing. That doesn't happen often. When it does, something important is going on.

Why Functional?

We need to write code that takes advantage of concurrency and parallelism.

The limitations of Imperative Languages

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.

Moving to Functional Programming

In FP, functions are the basic building blocks, values are immutable, and the code is declarative.

Working with Immutable Data

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]

Building Programs with Functions

  • 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
Pure functions
  • 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)
# => 4
Impure functions

More complex, unpredictable results and with side effects (Chapter 7)

Transforming Values

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

Declaring Code

  • Generates less and simpler code
  • Less code = fewer bugs
  • what's necessary to solve a problem instead of how to solve a problem( imperative)

Chapter 2 - Working with Variables and Functions

  • 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) ...

Common Types:

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

Common Operators

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}!"

Creating Logical Expressions

  • 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

Variables

  • Explicit names
  • snake_case format
iex> quantity = 10
10

iex> product_price = 11.5
11.5

iex> total_cost = product_price * quantity
115.0

Anonymous Functions

  • 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 as First-Class Citizens

  • 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

Sharing Values Without Using Arguments

  • 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

Naming Functions

  • 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" )

Elixir's Named Functions

  • Full Docs
  • Kernel module doesn't need the module name when called div(1,2)
  • All other modules do String.capitalize("HellO THerE")

Creating Modules and Functions

  • 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

Importing Named Functions

  • 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

Using Named Functions as Values

  • use the &function/arity operator to bind a named function to a variable
iex> upcase = &String.upcase/1
iex> upcase.("oi")
OI

Chapter 3 - Using Pattern Matching to Control the Program Flow

Elixir's pattern match shapes everything you program.

Making Two things Match

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

Unpacking Values from Various Data Types

Pattern matching is useful for extracting parts of values to variables in a process called destructing.

Matching Parts of a String

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)

Matching Tuples

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)

Matching Lists

  • 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] = []
** MatchError

Matching Maps

Maps 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)

Maps vs. Keyword Lists

  • 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

Matching Structs

  • 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
  • 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"]

Control Flow with Functions

  • 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

Applying Default Values for Functions

  • 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

Expanding Control with Guard Clauses

  • 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

Elixir Control-Flow Structures

  • 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

Case: Control with Pattern Matching

  • 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

Cond: Control with Logical Expressions

  • 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"

If: Taking a look at our old friend

  • 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)

Chapter 4 - Diving into Recursion

  • While we use for and while loops on imperative languages, we have recursion in FP

Surrounded by Boundaries

  • 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

Navigating Through Lists

  • 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

  • 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]
The Key-based Accessors
iex> item = %{a: 1, b: 2}
iex> item[:a]
1
iex> item.a
1
iex> item[:c]
nil
iex> item.c
** KeyError

Conquering Recursion

Decrease and Conquer

  • 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

Divide and Conquer

  • 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

Tail-Call Optimization

  • 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

Functions without borders

  • Unbounded recursion is when we can't predict the number of repetition for a recursive function
  • For example: web crawler, directory navigator

Making functions more predictable

  • Adding Boundaries
  • Avoiding Infinite Loops

Using Recursion with Anonymous Functions

  • 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)

Chapter 5 - Using Higher-Order Functions

  • 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)

Using the Enum Module

  • 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 Comprehensions

  • 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]

Pipelining Your Functions

  • 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

Be Lazy

  • Lazy operations provide alternative techniques of programming and creating efficient programs

Delay the Function Call

  • 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

Working with the Infinite

  • 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
  • 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)
Stream
  • 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"} ....]

Pipelining Data Streams

  • 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

Chapter 6 - Designing Your Elixir Applications

Starting your project with Mix

  • Command-line interface (CLI)
  • Provides essentials for developing any Elixir application
  • Helps creates and maintain projects (compile, debug, test, manage dependencies)
  • built-in Elixir

Running the new Task

  • 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

Create the Start Task

  • 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

Designing Entities with Structs

  • 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

  • alias directive can be used to create module shortcuts alias Mix.Shell.IO, as: Shell

Using Protocols to Create Polymorphic Functions

  • 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
end

Conventions

  • 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

Creating Module Behaviours

  • 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

Adding Type Specifications

  • Notations that say what your functions expect and return
  • Good for static check and documentation
  • use @type t directive inside the module
@type t :: %ModuleStruct{
    prop: Type.t
}
  • Use Dialyzer to statically analyze the code

Chapter 7 - Handling Impure Functions

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

Pure vs Impure Functions

  • 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

Pure

  • Always return consistent output when the same input is given, and no side effects are produced
  • They can produce errors, but those are predictable

Impure

  • 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

Trying, Rescuing, and Catching

  • Throwing values and/or raising errors is unusual in FP
  • Names from functions that raise errors end with !

Try, Raise and Rescue

  • 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

Try, Throw and Catch

  • 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

Handling Impure Functions with the Error Monad

  • 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

Using with

  • 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

About

Learn Functional Programming with Elixir Book

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Elixir 100.0%