
## Metadata
- Author:: [[Ulisses Almeida]]
- Full Title: Learn Functional Programming With Elixir
- Category: #books
## Literature Notes
```dataview
TABLE rows.file.link AS "Literature Note", rows.file.cday AS "Date"
FROM #note/literature AND [[Learn Functional Programming With Elixir]]
GROUP BY file.link
sort date ASCENDING
```
## Summaries
### Chapter 1: Thinking Functionally
In functional programming, functions are the basic building blocks of a program, values are immutable, and the code is declarative, rather than imperative. Functions, immutability, and declarative code are the core principles of functional programming.
Immutability is especially important when working with concurrency or parallel computation. Immutable values sidesteps the need for complex locks and mutexes commonly found in imperative languages.
**Pure functions** are functions with the following properties:
- values that are immutable
- results that are affected only by the function's arguments
- computation that doesn't have side effects
Elixir features the pipe operator `|>` that passes the result of each expression to the next function:
```elixir
def capitalize_words(title) do
title
|> String.splut
|> capitalize_all
|> join_with_whitespace
end
```
### Chapter 2: Working with Variables and Functions
#### Logical Expressions
`and`, `or`, and `not` work with boolean values - left side of expression must be a boolean value
`&&`, `||`, `!` allow the left side value to be truthy or falsy (`nil`, `false`)
- `&&` returns the right side expression result when the left value is truthy, else returns 1st left-side value
- `||` returns the first truthy expression, else returns first value
#### Anonymous Functions
`> hello = fn name -> "Hello #{name}!" end`
`hello` is a variable that we assign the function to
`name` is the parameter
`#{name}` is how one does string interpolation in Elixir
```bash
> hello.("Jane")
> Hello, Jane!
```
**Closures** allow you to share variables in same lexical scope.
#### Named Functions
Named functions are defined in [[Learn Functional Programming With Elixir#Modules|modules]], which must be named with atoms or **aliases** (any word starting with a capital letter, ASCII only). For named function arguments, you can omit parentheses.
```elixir
defmodule Checkout do
def total_cost(price, tax_rate) do
price * (tax_rate + 1)
end
end
```
```
iex> c("checkout.ex")
iex> Checkout.total_cost(100, 0.2)
120.0
```
#### Modules
Named functions live in modules. They're defined with `defmodule` and CamelCase is used to name them. The exist in a file where the filename is the same as the module name but all lower case.
Multiple module files can exist in the same directory - those modules will all belong to the same namespace which has the same name as the directory.
`work_with_functions/lib/ecommerce/checkout.ex`
```elixir
defmodule Ecommerce.Checkout do
def total_cost(price, tax_rate) do
price * (tax_rate + 1)
end
end
```
Module attributes start with `@` and can be used for constants, documentation, and more.
Modules need to be imported with name + **arity** (how many arguments a function received).
```elixir
defmodule TaskListWithImport do
import File, only: [write: 3, read: 1]
@file_name "task_list.md"
def add(task_name) do
task = "[ ] " <> task_name <> "\n"
write(@file_name, task, [:append])
end
def show_list do
read(@file_name)
end
end
```
#### Named Functions as Values
`&` is a shortcut for referencing a function, also known as a _capture operator_. You can bind it to a variable:
```
iex> upcase &String.upcase/1
iex> upcase.("Hello")
HELLO
```
#### Anonymous Functions as Values
`&` is also used here:
```elixir
iex> total_cost = &(&1 * &2)
iex> total_cost.(10, 2)
20
```
However, this can't be used for 0-arity functions.
### Chapter 3: Using Pattern Matching to Control the Program Flow
#### Pattern Matching
The = operator always does pattern matching, even in variable assignment. This is because if you try to match an empty variable or similar, Elixir will _make_ them match - by assigning the value on the right to the empty variable on the left.
#### Destructuring (aka Unpacking)
##### String
`"Auth: " <> credentials = "Auth: 793AG!"` will assign `"793AG!"` to `credentials`
##### Tuple
`{ability_score, _} = Integer.parse(user_input)`
This allows us to ignore second part of `user_input` by assigning it to `_`
##### Lists
`[a, a, a] = [1, 2, 1]` raises a `Match Error`
`[a, a, a] = [1, 1, 1]` works fine
##### Maps
Extract and check
```elixir
iex> %{intelligence: 10, dexterity: dexterity_value} = abilities
iex> dexterity_value
12
```
In this example, `abilities` must have key `intelligence` with value 10, then check if `abilities` has `dexterity` key then load that into `dexterity_value`. This *doesn't* create a new map.
##### Structs
Same as maps
Sigils are shortcuts to quickly create values. For example, a word-list: `~w(c, a, be)` gives `["c", "a", "be"]`.
### Flow Control
**Function clauses** are functions with the same name but are used with pattern matching in the parameters to control flow.
```elixir
defmodule NumberCompare do
def greater(number, other_number) do
check(number >= other_number, number, other_number)
end
defp check(true, number, _), do: number
defp check(false, _, other_number), do: other_number
end
```
In the above example, the private (`defp`) check functions are function clauses.
You can define default values for parameters in function signature with `\\`. Elixir will create multiple functions - one with a value-less argument, the other with no argument where Elixir will call the first version and pass in the default value.
```elixir
defmodule Checkout do
def total_cost(price, quantity \\ 10), do: price * quantity
end
```
**Guard clauses** allow you to provide boolean statements that must be true for the function to be called, using `when`.
```elixir
defmodule NumberCompare do
def greater(number, other_number) when number >= other_number, do: number
def greater(_, other_number), do: other_number
end
```
The first version of `greater` is only called if the first argument is greater than or equal to the second one.
`defguard` is a directive allows you to create macro functions to be used in guard clauses.
```
defmodule Checkout do
defguard is_rate(value) when is_float(value) and value >= 0 and value <= 1
defguard is_cents(value) when is_integeer(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
```
`case` lets you check against multiple pattern match clauses. ^g1u06r
```elixir
user_input = IO.gets "Write your ability score:\n"
case Integer.parse(user_input) do
:error -> IO.puts "Invalid ability score: #{user_input}"
{ability_score, _} ->
ability_modifier = (ability_score - 10) / 2
IO.puts "Your ability modifier is #{ability_modifier}"
end
```
This handles 2 scenarios: user input is a valid integer, and user input is not a valid integer. patterh matching expression comes before the `->` and the expression after it is evaluated.
`cond` allows you to check against logical expressions
```elixir
{age, _} = Integer.parse IO.gets("Person's age:\n")
result = cond do
age < 13 -> "kid"
age <= 18 -> "teen"
age > 18 -> "adult"
end
IO.puts "Result: #{result}"
```
Notice the similarity in structure to [[Learn Functional Programming With Elixir#^g1u06r|case]].
`if` and `unless` are the same as in other languages.
### Chapter 4: Diving into Recursion
**Bounded recursion** - a type of recursive function in which successive calls to itself have an end, and is the most common type of recursive function. Boundary clauses in elixir should *always* be defined before the clauses that can be repeated.
```elixir
defmodule Sum do
def up_to(0), do: 0
def up_to(n), do: n + up_to(n - 1)
end
```
#### Handling Lists
For lists, you can recursively process them with this syntax: `[head | tail]`, which saves the first item in the list to `head` and the rest to `tail`. Here's a simple example:
```elixir
defmodule Math do
def sum([]), do: 0
def sum([head | tail]), do : head + sum(tail)
end
```
`[head | tail]` is also useful for making new lists, which prepends an element to the original list (creating a new list, since data is immutable) and is faster than appending with `++`.
```elixir
@enchanter_name "Edwin"
def enchant_for_sale([]), do: []
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
```
In the recursive step, we use pattern matching to retrieve the head of the input list (`item`), then create a new item by applying transformation rules to it. Then we build a new list where the head is `new_item` and the rest of it is a recursive call to `enchant_for_sale/1`.
You can also use *filter function clauses* to use pattern matching and prevent a transformation from happening. Using the above example, we want to avoid enchanting an item that is already enchanted, and we'll do that by checking that `magic` is already `true` (L4-6):
```elixir
@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
```
#### Conquering Recursion
**Decrease and conquer** is a technique for solving problems using recursion by reducing a problem to its simplest form (the *base case*) and solving it incrementally. Here's an example starting with a hardcoded factorial solution and breaking it down to the recursive form starting with the base case.
```elixir
defmodule Factorial do
def of(0), do: 1
def of(1), do: 1
def of(2), do: 2 * 1
def of(3), do: 3 * 2 * 1
def of(4), do: 4 * 3 * 2 * 1
end
```
This is hardcoded, but not generalized. `Factorial.of(5)` won't work. But we can start simplifying this. Looking at the patterns above, we can see that calling the function with n - 1 will get us the bulk of the result for n, so we can replace those with function calls:
```elixir
defmodule Factorial do
def of(0), do: 1
def of(1), do: 1 * of(0)
def of(2), do: 2 * of(1)
def of(3), do: 3 * of(2)
def of(4), do: 4 * of(3)
end
```
The pattern is more clear now, and we can collapse and generalize it like this: ^45yua2
```elixir
defmodule Factorial do
def of(0), do: 1
def of(n) when n > 0, do: n * of(n-1)
end
```
**Divide and conquer** is another recursive technique that is about separating the problem into 2 or more parts that can be processed on their own and then combined later. Consider sorting a list: in functional programming, data is immutable so we can't change the order of items in a list directly. Instead we can keep dividing the list in half until we have a bunch of one-item lists (which are by definition sorted), then we have to merge them in a sorted way.
To divide a list in half recursively, we will make use of `Enum.split/2`, `Enum.count/1` to calculate the size of the list, and `Kernel.div/2` to do integer division to get the split point.
```elixir
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.splut(list, half_size)
# We need to sort list_a and list_b
# ascending(list_a)
# ascending(list_b)
# And merge them using some strategy
end
end
```
For the merge, we need a function that will unify two lists by putting the smallest elements at the beginning. We'd want something to look like:
```
merge([5, 9], [1, 4, 5])
[1 | merge([5, 9], [4, 5])]
[1, 4 | merge([5, 9], [5])]
[1, 4, 5 | merge([9], [5])]
[1, 4, 5, 5, | merge([9])]
[1, 4, 5, 5, 9]
```
Here's a merge function that could do this:
```elixir
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)]
defp merge(list_a = [head_a | _], [head_b | tail_b]) when head_a > head_b do [head_b | merge(list_a, tail_b)]
end
```
The two main versions simply take the smallest head item and then merge the remaining ones. Now it just needs to be called in the `ascending` function.
```elixir
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.splut(list, half_size)
merge(
ascending(list_a),
ascending(list_b)
)
end
end
```
The recursive calls for `ascending` can happen in parallel because they're completely independent.
#### Tail-Call Optimization
**Tail-call optimization** is a common compiler feature in functional languages that reduces functions in memory without allocating any more. To use it, you need to ensure that the last expression in a function is a function call. If it is, then the current function's return is the return of the new function call, so the current function doesn't need to stay in memory.
```
iex> scream = fn word -> String.upcase("#{word}!!!") end
iex> scream.("help")
"HELP!!!"
```
The result of `scream.("help")` is the same as `String.upcase("help!!!")` so the compiler can remove `scream.("help")` from the function call stack.
The [[Learn Functional Programming With Elixir#^45yua2|previously seen factorial example]] is **body recursive** - it doesn't end with a function call and so can't take advantage of tail-call optimization. Calling it with a large number will result in heavy memory usage.
The typical way to create tail-recursive functions is to replace the use of the function result with an extra argument that accumulates the results of each iteration:
```elixir
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
```
#### Functions Without Borders
*Unbounded recursion* is when you can't predict the number of repetitions for a recursive function, such as a web crawler. You can add stop clauses (such as for a directory crawler, an empty directory, or a maximum depth) to create an arbitrary or user-defined boundary.
```elixir
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 # underscore indicates the parameter won't be used
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
```
To add something like a max depth restriction, you would create a module attribute (`@max_depth 2`),
```elixir
defmodule DepthNavigator do
@max_depth 2
def navigate(dir) do
expanded_dir = Path.expand(dir)
go_through([expanded_dir], 0) # add initial value for depth from entry point
end
defp go_through([], _current_depth), do: nil
defp go_through(_dirs, current_depth) when current_depth > @max_depth, do: nil # This is the boundary check
defp go_through([content | rest], current_depth) do
print_and_navigate(content, File.dir?(content))
go_through(rest, current_depth)
end
defp print_and_navigate(_dir, false), do: nil # underscore indicates the parameter won't be used
defp print_and_navigate(dir, true) do
IO.puts dir
children_dirs = File.ls!(dir)
go_through(expand_dirs(children_dirs, dir), current_depth + 1)
end
end
```
In this, we created a module attribute of max_depth, then updated `go_through` to take a `current_depth` parameter, a guard clause for if the max depth has been exceeded, and then recursive calls to `go_through` increment the `current_depth`.
In this kind of recursion, you'll also want to avoid infinite loops. For file navigation, this can happen with symbolic links, or symlinks. We can avoid following symlinks by checking if an entity detected in the file system is a directory or not:
```elixir
def dir?(dir) do
{:ok, %{type: type}} = File.lstat(dir)
type == :directory
end
```
#### Using Recursion with Anonymous Functions
Calling a function recursively is more difficult when it is anonymous. To solve this, you can wrap the anonymous function in another function and delay recursive call execution.
What doesn't work:
```
iex> factorial = fn
0 -> 1
x when x > 0 -> x * factorial.(x - 1)
end
** (CompileError) undefined function factorial/0
```
Wrapping solution:
```
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)
120
iex> factorial.(10)
3628800
```
This is less expressive and more complex - but doable.
> [!NOTE] Using the capturing feature to use named function references like anon functions
> iex> c("factorial.ex")
> iex> factorial = &Factorial.of/1
> iex> factorial.(5)
> 120
### Chapter 5: Using Higher-Order Functions