![rw-book-cover](https://m.media-amazon.com/images/I/91B-u7PRbEL._SY160.jpg) ## 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