├── README.md ├── ch02 ├── PatternMatching-1 │ └── README.md ├── PatternMatching-2 │ └── README.md └── PatternMatching-3 │ └── README.md ├── ch05 ├── Functions-1 │ └── README.md ├── Functions-2 │ └── README.md ├── Functions-3 │ └── README.md ├── Functions-4 │ └── README.md └── Functions-5 │ └── README.md ├── ch06 ├── ModulesAndFunctions-1 │ └── README.md ├── ModulesAndFunctions-2 │ └── README.md ├── ModulesAndFunctions-3 │ └── README.md ├── ModulesAndFunctions-4 │ ├── README.md │ └── sum.exs ├── ModulesAndFunctions-5 │ ├── README.md │ └── gcd.exs ├── ModulesAndFunctions-6 │ ├── README.md │ └── chop.exs ├── ModulesAndFunctions-7 │ └── README.md └── times.exs ├── ch07 ├── ListsAndRecursion-1 │ ├── README.md │ └── my_list.exs ├── ListsAndRecursion-2 │ ├── README.md │ └── my_list.exs ├── ListsAndRecursion-3 │ ├── README.md │ └── my_list.exs └── ListsAndRecursion-4 │ ├── README.md │ └── my_list.exs ├── ch10 ├── ListsAndRecursion-5 │ ├── README.md │ └── my_enum.exs ├── ListsAndRecursion-6 │ ├── README.md │ └── my_list.exs ├── ListsAndRecursion-7 │ ├── README.md │ └── prime_finder.exs └── ListsAndRecursion-8 │ ├── README.md │ └── price_calculator.exs ├── ch11 ├── StringsAndBinaries-1 │ ├── README.md │ └── my_string.exs ├── StringsAndBinaries-2 │ ├── README.md │ └── my_string.exs ├── StringsAndBinaries-3 │ └── README.md ├── StringsAndBinaries-4 │ ├── README.md │ └── calculator.exs ├── StringsAndBinaries-5 │ ├── README.md │ └── strings.exs ├── StringsAndBinaries-6 │ ├── README.md │ └── sentences.exs └── StringsAndBinaries-7 │ ├── README.md │ ├── orders.csv │ └── price_calculator.exs ├── ch12 ├── ControlFlow-1 │ ├── README.md │ └── fizzbuzz.exs ├── ControlFlow-2 │ └── README.md └── ControlFlow-3 │ └── README.md ├── ch13 ├── OrganizingAProject-1 │ ├── README.md │ └── issues │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── issues.ex │ │ └── issues │ │ │ └── cli.ex │ │ ├── mix.exs │ │ └── test │ │ ├── cli_test.exs │ │ ├── issues_test.exs │ │ └── test_helper.exs ├── OrganizingAProject-2 │ ├── README.md │ └── issues │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── issues.ex │ │ └── issues │ │ │ └── cli.ex │ │ ├── mix.exs │ │ └── test │ │ ├── cli_test.exs │ │ ├── issues_test.exs │ │ └── test_helper.exs ├── OrganizingAProject-3 │ ├── README.md │ └── issues │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── issues.ex │ │ └── issues │ │ │ ├── cli.ex │ │ │ └── github_issues.ex │ │ ├── mix.exs │ │ ├── mix.lock │ │ └── test │ │ ├── cli_test.exs │ │ ├── issues_test.exs │ │ └── test_helper.exs ├── OrganizingAProject-4 │ ├── README.md │ └── issues │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── issues.ex │ │ └── issues │ │ │ ├── cli.ex │ │ │ ├── formatter.ex │ │ │ └── github_issues.ex │ │ ├── mix.exs │ │ ├── mix.lock │ │ └── test │ │ ├── cli_test.exs │ │ ├── issues_test.exs │ │ └── test_helper.exs └── OrganizingAProject-6 │ ├── README.md │ └── weather │ ├── .gitignore │ ├── config │ └── config.exs │ ├── lib │ └── weather.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── test_helper.exs │ └── weather_test.exs ├── ch14 ├── WorkingWithMultipleProcesses-1 │ ├── README.md │ └── chain.exs ├── WorkingWithMultipleProcesses-2 │ ├── README.md │ └── processes.exs ├── WorkingWithMultipleProcesses-3 │ ├── README.md │ └── processes.exs ├── WorkingWithMultipleProcesses-4 │ ├── README.md │ └── processes.exs ├── WorkingWithMultipleProcesses-5 │ ├── README.md │ ├── processes_ex.exs │ └── processes_no_ex.exs ├── WorkingWithMultipleProcesses-6 │ ├── README.md │ └── parallel.exs ├── WorkingWithMultipleProcesses-7 │ ├── README.md │ └── parallel.exs ├── WorkingWithMultipleProcesses-8 │ ├── README.md │ └── fibonacci.exs └── WorkingWithMultipleProcesses-9 │ ├── .gitignore │ ├── README.md │ ├── generate_test_files.py │ └── word_count.exs ├── ch15 ├── Nodes-1 │ └── README.md ├── Nodes-2 │ └── README.md ├── Nodes-3 │ ├── README.md │ └── ticker.exs └── Nodes-4 │ ├── README.md │ └── ticker.exs ├── ch16 ├── OTP-Servers-1 │ ├── README.md │ └── stack │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── stack.ex │ │ └── stack │ │ │ └── server.ex │ │ ├── mix.exs │ │ └── test │ │ ├── stack_test.exs │ │ └── test_helper.exs ├── OTP-Servers-2 │ ├── README.md │ └── stack │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── stack.ex │ │ └── stack │ │ │ └── server.ex │ │ ├── mix.exs │ │ └── test │ │ ├── stack_test.exs │ │ └── test_helper.exs ├── OTP-Servers-3 │ ├── README.md │ └── stack │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── stack.ex │ │ └── stack │ │ │ └── server.ex │ │ ├── mix.exs │ │ └── test │ │ ├── stack_test.exs │ │ └── test_helper.exs ├── OTP-Servers-4 │ ├── README.md │ └── stack │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── stack.ex │ │ └── stack │ │ │ └── server.ex │ │ ├── mix.exs │ │ └── test │ │ ├── stack_test.exs │ │ └── test_helper.exs └── OTP-Servers-5 │ ├── README.md │ └── stack │ ├── .gitignore │ ├── config │ └── config.exs │ ├── lib │ ├── stack.ex │ └── stack │ │ └── server.ex │ ├── mix.exs │ └── test │ ├── stack_test.exs │ └── test_helper.exs ├── ch17 ├── OTP-Supervisors-1 │ ├── README.md │ └── stack │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── stack.ex │ │ └── stack │ │ │ └── server.ex │ │ ├── mix.exs │ │ └── test │ │ ├── stack_test.exs │ │ └── test_helper.exs └── OTP-Supervisors-2 │ ├── README.md │ └── stack │ ├── .gitignore │ ├── config │ └── config.exs │ ├── lib │ ├── stack.ex │ └── stack │ │ ├── server.ex │ │ ├── stack_supervisor.ex │ │ ├── stash.ex │ │ └── stash_supervisor.ex │ ├── mix.exs │ └── test │ ├── stack_test.exs │ └── test_helper.exs ├── ch18 ├── OTP-Applications-1 │ └── README.md ├── OTP-Applications-2 │ ├── README.md │ └── stack │ │ ├── .gitignore │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── stack.ex │ │ └── stack │ │ │ ├── server.ex │ │ │ ├── stack_supervisor.ex │ │ │ ├── stash.ex │ │ │ └── stash_supervisor.ex │ │ ├── mix.exs │ │ └── test │ │ ├── server_test.exs │ │ ├── stack_test.exs │ │ ├── stash_test.exs │ │ └── test_helper.exs └── OTP-Applications-3 │ ├── README.md │ └── sequence │ ├── .gitignore │ ├── config │ └── config.exs │ ├── lib │ ├── sequence.ex │ └── sequence │ │ ├── server.ex │ │ ├── stash.ex │ │ ├── subsupervisor.ex │ │ └── supervisor.ex │ ├── mix.exs │ ├── new_server.ex │ └── test │ ├── sequence_test.exs │ └── test_helper.exs ├── ch20 ├── MacrosAndCodeEvaluation-1 │ ├── README.md │ └── my.exs ├── MacrosAndCodeEvaluation-2 │ ├── README.md │ └── times.exs └── MacrosAndCodeEvaluation-3 │ ├── README.md │ └── arithmetic.exs ├── ch21 ├── LinkingModules-BehavioursAndUse-1 │ └── README.md ├── LinkingModules-BehavioursAndUse-2 │ ├── README.md │ └── colorize.exs └── LinkingModules-BehavioursAndUse-3 │ ├── README.md │ └── tracer.exs └── ch22 ├── Protocols-1 ├── README.md └── caesar.exs ├── Protocols-2 ├── README.md ├── caesar.exs └── words.txt ├── Protocols-3 ├── README.md └── my_enum.exs └── Protocols-4 └── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Programming Elixir Exercises 2 | I recently purchased a few books in order to learn more about Elixir. The first one I am reading is *Programming Elixir*. This repository contains my solutions to all of the exercises as I work through them. I will try to write them in a manner to where they could be helpful to others in case someone is searching for a solution and finds mine. 3 | 4 | ## Index 5 | ### Chapter 2 6 | - [PatternMatching-1](./ch02/PatternMatching-1/README.md) 7 | - [PatternMatching-2](./ch02/PatternMatching-2/README.md) 8 | - [PatternMatching-3](./ch02/PatternMatching-3/README.md) 9 | 10 | ### Chapter 5 11 | - [Functions-1](./ch05/Functions-1/README.md) 12 | - [Functions-2](./ch05/Functions-2/README.md) 13 | - [Functions-3](./ch05/Functions-3/README.md) 14 | - [Functions-4](./ch05/Functions-4/README.md) 15 | - [Functions-5](./ch05/Functions-5/README.md) 16 | 17 | ### Chapter 6 18 | - [ModulesAndFunctions-1](./ch06/ModulesAndFunctions-1/README.md) 19 | - [ModulesAndFunctions-2](./ch06/ModulesAndFunctions-2/README.md) 20 | - [ModulesAndFunctions-3](./ch06/ModulesAndFunctions-3/README.md) 21 | - [ModulesAndFunctions-4](./ch06/ModulesAndFunctions-4/README.md) 22 | - [ModulesAndFunctions-5](./ch06/ModulesAndFunctions-5/README.md) 23 | - [ModulesAndFunctions-6](./ch06/ModulesAndFunctions-6/README.md) 24 | - [ModulesAndFunctions-7](./ch06/ModulesAndFunctions-7/README.md) 25 | 26 | ### Chapter 7 27 | - [ListsAndRecursion-1](./ch07/ListsAndRecursion-1/README.md) 28 | - [ListsAndRecursion-2](./ch07/ListsAndRecursion-2/README.md) 29 | - [ListsAndRecursion-3](./ch07/ListsAndRecursion-3/README.md) 30 | - [ListsAndRecursion-4](./ch07/ListsAndRecursion-4/README.md) 31 | 32 | ### Chapter 10 33 | - [ListsAndRecursion-5](./ch10/ListsAndRecursion-5/README.md) 34 | - [ListsAndRecursion-6](./ch10/ListsAndRecursion-6/README.md) 35 | - [ListsAndRecursion-7](./ch10/ListsAndRecursion-7/README.md) 36 | - [ListsAndRecursion-8](./ch10/ListsAndRecursion-8/README.md) 37 | 38 | ### Chapter 11 39 | - [StringsAndBinaries-1](./ch11/StringsAndBinaries-1/README.md) 40 | - [StringsAndBinaries-2](./ch11/StringsAndBinaries-2/README.md) 41 | - [StringsAndBinaries-3](./ch11/StringsAndBinaries-3/README.md) 42 | - [StringsAndBinaries-4](./ch11/StringsAndBinaries-4/README.md) 43 | - [StringsAndBinaries-5](./ch11/StringsAndBinaries-5/README.md) 44 | - [StringsAndBinaries-6](./ch11/StringsAndBinaries-6/README.md) 45 | - [StringsAndBinaries-7](./ch11/StringsAndBinaries-7/README.md) 46 | 47 | ### Chapter 12 48 | - [ControlFlow-1](./ch12/ControlFlow-1/README.md) 49 | - [ControlFlow-2](./ch12/ControlFlow-2/README.md) 50 | - [ControlFlow-3](./ch12/ControlFlow-3/README.md) 51 | 52 | ### Chapter 13 53 | - [OrganizingAProject-1](./ch13/OrganizingAProject-1/README.md) 54 | - [OrganizingAProject-2](./ch13/OrganizingAProject-2/README.md) 55 | - [OrganizingAProject-3](./ch13/OrganizingAProject-3/README.md) 56 | - [OrganizingAProject-4](./ch13/OrganizingAProject-4/README.md) 57 | - [OrganizingAProject-6](./ch13/OrganizingAProject-6/README.md) 58 | 59 | ### Chapter 14 60 | - [WorkingWithMultipleProcesses-1](./ch14/WorkingWithMultipleProcesses-1/README.md) 61 | - [WorkingWithMultipleProcesses-2](./ch14/WorkingWithMultipleProcesses-2/README.md) 62 | - [WorkingWithMultipleProcesses-3](./ch14/WorkingWithMultipleProcesses-3/README.md) 63 | - [WorkingWithMultipleProcesses-4](./ch14/WorkingWithMultipleProcesses-4/README.md) 64 | - [WorkingWithMultipleProcesses-5](./ch14/WorkingWithMultipleProcesses-5/README.md) 65 | - [WorkingWithMultipleProcesses-6](./ch14/WorkingWithMultipleProcesses-6/README.md) 66 | - [WorkingWithMultipleProcesses-7](./ch14/WorkingWithMultipleProcesses-7/README.md) 67 | - [WorkingWithMultipleProcesses-8](./ch14/WorkingWithMultipleProcesses-8/README.md) 68 | - [WorkingWithMultipleProcesses-9](./ch14/WorkingWithMultipleProcesses-9/README.md) 69 | 70 | ### Chapter 15 71 | - [Nodes-1](./ch15/Nodes-1/README.md) 72 | - [Nodes-2](./ch15/Nodes-2/README.md) 73 | - [Nodes-3](./ch15/Nodes-3/README.md) 74 | - [Nodes-4](./ch15/Nodes-4/README.md) 75 | 76 | ### Chapter 16 77 | - [OTP-Servers-1](./ch16/OTP-Servers-1/README.md) 78 | - [OTP-Servers-2](./ch16/OTP-Servers-2/README.md) 79 | - [OTP-Servers-3](./ch16/OTP-Servers-3/README.md) 80 | - [OTP-Servers-4](./ch16/OTP-Servers-4/README.md) 81 | - [OTP-Servers-5](./ch16/OTP-Servers-5/README.md) 82 | 83 | ### Chapter 17 84 | - [OTP-Supervisors-1](./ch17/OTP-Supervisors-1/README.md) 85 | - [OTP-Supervisors-2](./ch17/OTP-Supervisors-2/README.md) 86 | 87 | ### Chapter 18 88 | - [OTP-Applications-1](./ch18/OTP-Applications-1/README.md) 89 | - [OTP-Applications-2](./ch18/OTP-Applications-2/README.md) 90 | - [OTP-Applications-3](./ch18/OTP-Applications-3/README.md) 91 | 92 | ### Chapter 20 93 | - [MacrosAndCodeEvaluation-1](./ch20/MacrosAndCodeEvaluation-1/README.md) 94 | - [MacrosAndCodeEvaluation-2](./ch20/MacrosAndCodeEvaluation-2/README.md) 95 | - [MacrosAndCodeEvaluation-3](./ch20/MacrosAndCodeEvaluation-3/README.md) 96 | 97 | ### Chapter 21 98 | - [LinkingModules-BehavioursAndUse-1](./ch21/LinkingModules-BehavioursAndUse-1/README.md) 99 | - [LinkingModules-BehavioursAndUse-2](./ch21/LinkingModules-BehavioursAndUse-2/README.md) 100 | - [LinkingModules-BehavioursAndUse-3](./ch21/LinkingModules-BehavioursAndUse-3/README.md) 101 | 102 | ### Chapter 22 103 | - [Protocols-1](./ch22/Protocols-1/README.md) 104 | - [Protocols-2](./ch22/Protocols-2/README.md) 105 | - [Protocols-3](./ch22/Protocols-3/README.md) 106 | - [Protocols-4](./ch22/Protocols-4/README.md) 107 | -------------------------------------------------------------------------------- /ch02/PatternMatching-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: PatternMatching-1 2 | Which of the following will match? 3 | ``` 4 | a = [1, 2, 3] # Yes, a => [1, 2, 3] 5 | a = 4 # Yes, a => 4 6 | 4 = a # Yes 7 | [a, b] = [1, 2, 3] # No 8 | a = [[1, 2, 3]] # Yes, a => [[1, 2, 3]] 9 | [a] = [[1, 2, 3]] # Yes, a => [1, 2, 3] 10 | [[a]] = [[1, 2, 3]] # No 11 | ``` 12 | -------------------------------------------------------------------------------- /ch02/PatternMatching-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: PatternMatching-2 2 | Which of the following will match? 3 | ``` 4 | [a, b, a] = [1, 2, 3] # No, a already assigned 1 5 | [a, b, a] = [1, 1, 2] # No, a already assigned 1 6 | [a, b, a] = [1, 2, 1] # Yes, a => 1, b => 2 7 | ``` 8 | -------------------------------------------------------------------------------- /ch02/PatternMatching-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: PatternMatching-3 2 | If you assume the variable *a* initially contains the value 2, which of the following will match? 3 | ``` 4 | [a, b, a] = [1, 2, 3] # No, a already assigned 1 5 | [a, b, a] = [1, 1, 2] # No, a already assigned 1 6 | a = 1 # Yes, a => 1 7 | ^a = 2 # No, a was just assigned 1. It is pinned so it isn't reassigned 2. 8 | ^a = 1 # Yes 9 | ^a = 2 - a # Yes, 1 = 2 - 1 10 | ``` 11 | -------------------------------------------------------------------------------- /ch05/Functions-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: Functions-1 2 | Go into iex. Create and run the functions that do the following: 3 | 4 | ``` 5 | list_concat.([:a, :b], [:c, :d]) # => [:a, :b, :c, :d] 6 | sum.(1, 2, 3) # => 6 7 | pair_tuple_to_list.({1234, 5678}) # => [1234, 5678] 8 | ``` 9 | 10 | ## Solution 11 | 12 | ``` 13 | iex> list_concat = fn list_a, list_b -> list_a ++ list_b end 14 | #Function<12.50752066/2 in :erl_eval.expr/5> 15 | iex> list_concat.([:a, :b], [:c, :d]) 16 | [:a, :b, :c, :d] 17 | 18 | iex> sum = fn num_a, num_b, num_c -> num_a + num_b + num_c end 19 | #Function<18.50752066/3 in :erl_eval.expr/5> 20 | iex> sum.(1, 2, 3) 21 | 6 22 | 23 | iex> pair_tuple_to_list = fn {a, b} -> [a, b] end 24 | #Function<6.50752066/1 in :erl_eval.expr/5> 25 | iex> pair_tuple_to_list.({1234, 5678}) 26 | [1234, 5678] 27 | ``` 28 | -------------------------------------------------------------------------------- /ch05/Functions-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: Functions-2 2 | Write a function that takes three arguments. If the first two are zero, return "FizzBuzz." If the first is zero, return "Fizz." If the second is zero, return "Buzz." Otherwise, return the third argument. Do not use any language features that we haven't yet covered in this book. 3 | 4 | ## Solution 5 | 6 | ``` 7 | iex> fizz_buzz = fn 8 | ...> 0, 0, c -> "FizzBuzz" 9 | ...> 0, b, c -> "Fizz" 10 | ...> a, 0, c -> "Buzz" 11 | ...> a, b, c -> c 12 | ...> end 13 | #Function<18.50752066/3 in :erl_eval.expr/5> 14 | iex> fizz_buzz.(0, 0, 3) 15 | "FizzBuzz" 16 | iex> fizz_buzz.(1, 0, 3) 17 | "Buzz" 18 | iex> fizz_buzz.(0, 2, 3) 19 | "Fizz" 20 | iex> fizz_buzz.(1, 2, 3) 21 | 3 22 | ``` 23 | -------------------------------------------------------------------------------- /ch05/Functions-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: Functions-3 2 | The operator *rem(a, b)* returns the remainder after dividing *a* by *b*. Write a function that takes a single integer (*n*) and calls the function in the previous exercise, passing it *rem(n, 3)*, *rem(n, 5)*, and *n*. Call it seven times with the arguments 10, 11, 12, and so on. You should get "Buzz, 11, Fizz, 13, 14, FizzBuzz, 16". 3 | 4 | ## Solution 5 | 6 | Recall the function we wrote in the previous exercise: 7 | ``` 8 | fizz_buzz = fn 9 | 0, 0, c -> "FizzBuzz" 10 | 0, b, c -> "Fizz" 11 | a, 0, c -> "Buzz" 12 | a, b, c -> c 13 | end 14 | ``` 15 | 16 | Let's rename it since it isn't actually the complete FizzBuzz solution: 17 | ``` 18 | fizz_buzz_helper = fn 19 | 0, 0, c -> "FizzBuzz" 20 | 0, b, c -> "Fizz" 21 | a, 0, c -> "Buzz" 22 | a, b, c -> c 23 | end 24 | ``` 25 | 26 | Then our solution to this problem will look like: 27 | ``` 28 | fizz_buzz = fn n -> fizz_buzz_helper.(rem(n, 3), rem(n,5), n) end 29 | ``` 30 | 31 | Let's call it seven times and check. 32 | ``` 33 | 10..16 |> Enum.to_list |> Enum.map(fn(n) -> fizz_buzz.(n) end) 34 | ``` 35 | 36 | Put it all together in *iex* and we see: 37 | ``` 38 | iex> fizz_buzz_helper = fn 39 | ...> 0, 0, c -> "FizzBuzz" 40 | ...> 0, b, c -> "Fizz" 41 | ...> a, 0, c -> "Buzz" 42 | ...> a, b, c -> c 43 | ...> end 44 | #Function<18.50752066/3 in :erl_eval.expr/5> 45 | iex> fizz_buzz = fn n -> fizz_buzz_helper.(rem(n, 3), rem(n,5), n) end 46 | #Function<6.50752066/1 in :erl_eval.expr/5> 47 | iex> 10..16 |> Enum.to_list |> Enum.map(fn(n) -> fizz_buzz.(n) end) 48 | ["Buzz", 11, "Fizz", 13, 14, "FizzBuzz", 16] 49 | ``` 50 | -------------------------------------------------------------------------------- /ch05/Functions-4/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: Functions-4 2 | Write a function *prefix* that takes a string. It should return a new function that takes a second string. When that second function is called, it will return a string containing the first string, a space, and the second string. 3 | 4 | ## Solution 5 | 6 | First things first, we know the function needs to take a string as a parameter and return a function that also takes a string as a parameter: 7 | ``` 8 | func = fn str_a -> 9 | fn str_b -> 10 | # TODO 11 | end 12 | end 13 | ``` 14 | 15 | Cool, now let's add the functionality to the inner-function. 16 | ``` 17 | func = fn str_a -> 18 | fn str_b -> 19 | "#{str_a} #{str_b}" 20 | end 21 | end 22 | ``` 23 | 24 | It should be functional, but let's name things more intuitively now that we know what it's purpose is. 25 | ``` 26 | prefix = fn prefix_string -> 27 | fn postfix_string -> 28 | "#{prefix_string} #{postfix_string}" 29 | end 30 | end 31 | ``` 32 | 33 | And let's test it out! 34 | ``` 35 | iex> prefix = fn prefix_string -> 36 | ...> fn postfix_string -> 37 | ...> "#{prefix_string} #{postfix_string}" 38 | ...> end 39 | ...> end 40 | #Function<6.50752066/1 in :erl_eval.expr/5> 41 | iex> mrs = prefix.("Mrs") 42 | #Function<6.50752066/1 in :erl_eval.expr/5> 43 | iex> mrs.("Smith") 44 | "Mrs Smith" 45 | ``` 46 | 47 | Magnificent. 48 | -------------------------------------------------------------------------------- /ch05/Functions-5/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: Functions-5 2 | Use the &... notation to rewrite the following. 3 | ``` 4 | Enum.map [1, 2, 3, 4], fn x -> x + 2 end 5 | Enum.each [1, 2, 3, 4], fn x -> IO.inspect x end 6 | ``` 7 | 8 | ## Solution 9 | There isn't much to this one. First, let's replace the *fn x -> ... end* with: 10 | ``` 11 | &(...) 12 | ``` 13 | 14 | The function takes one parameter, which we can reference using *&1*, and then we just need to add *2* to it. 15 | ``` 16 | &(&1 + 2) 17 | ``` 18 | 19 | Plug that back in and we get: 20 | ``` 21 | Enum.map [1, 2, 3, 4], &(&1 + 2) 22 | ``` 23 | 24 | Let's do the same thing for the second line: 25 | ``` 26 | Enum.each [1, 2, 3, 4], &(IO.inspect &1) 27 | ``` 28 | 29 | And now, let's test it: 30 | ``` 31 | iex> Enum.map [1, 2, 3, 4], fn x -> x + 2 end 32 | [3, 4, 5, 6] 33 | iex> Enum.map [1, 2, 3, 4], &(&1 + 2) 34 | [3, 4, 5, 6] 35 | 36 | iex> Enum.each [1, 2, 3, 4], fn x -> IO.inspect x end 37 | 1 38 | 2 39 | 3 40 | 4 41 | :ok 42 | iex> Enum.each [1, 2, 3, 4], &(IO.inspect &1) 43 | 1 44 | 2 45 | 3 46 | 4 47 | :ok 48 | ``` 49 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ModulesAndFunctions-1 2 | Extend the *Times* module with a *triple* function that multiplies its parameter by three. 3 | 4 | ## Solution 5 | See the [Times.exs](../times.exs) file. Since exercises 1-3 all use this file, it has been placed in the parent directory. 6 | 7 | We added the function definition: 8 | ``` 9 | def triple(n), do: n * 3 10 | ``` 11 | 12 | Let's run it in *iex*: 13 | ``` 14 | iex> Times.triple 5 15 | 15 16 | ``` 17 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ModulesAndFunctions-2 2 | Run the result in *iex*. Use both techniques to compile the file. 3 | 4 | ## Solution 5 | Launching *iex* with the filename: 6 | ``` 7 | $ iex times.exs 8 | Erlang/OTP 18 [erts-7.3] [source-d2a6d81] [64-bit] [async-threads:10] [hipe] [kernel-poll:false] 9 | 10 | iex> Times.triple 5 11 | 15 12 | ``` 13 | 14 | Compiling it within *iex*: 15 | ``` 16 | iex> c "times.exs" 17 | [Times] 18 | iex> Times.triple 5 19 | 15 20 | ``` 21 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ModulesAndFunctions-3 2 | Add a *quadruple* function. (Maybe it could call the *double* function...) 3 | 4 | ## Solution 5 | See the [Times.exs](../times.exs) file. 6 | 7 | We added the function definition: 8 | ``` 9 | def quadruple(n), do: n |> double |> double 10 | ``` 11 | 12 | It calls *double* for the sake of using another function within the module; however, realistically it would be simpler to define it as: 13 | ``` 14 | def quadruple(n), do: n * 4 15 | ``` 16 | 17 | Let's run it: 18 | ``` 19 | iex> Times.quadruple 5 20 | 20 21 | ``` 22 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-4/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ModulesAndFunctions-4 2 | Implement and run a function *sum(n)* that uses recursion to calculate the sum of the integers from 1 to *n*. You'll need to write this function inside a module in a separate file. Then load up *iex*, compile that file, and try your function. 3 | 4 | ## Solution 5 | See the [sum.exs](./sum.exs) file for the full module. 6 | 7 | There are only two necessary cases; however we will implement three. 8 | 9 | The base case: 10 | ``` 11 | defp do_calculate(0), do: 0 12 | ``` 13 | 14 | A second base case: 15 | ``` 16 | defp do_calculate(1), do: 1 17 | ``` 18 | 19 | And the main case: 20 | ``` 21 | defp do_calculate(n), do: n + do_calculate(n - 1) 22 | ``` 23 | 24 | The second base case is not actually necessary but adding 0 to the sum has no effect so this simply prevents us from doing that. 25 | 26 | Lastly, add a non-private function with a guard clause to prevent invalid parameters and presto: 27 | ``` 28 | iex> Sum.calculate 3 29 | 6 30 | iex> Sum.calculate 5 31 | 15 32 | iex> Sum.calculate -4 33 | {:error, "Expected a positive integer."} 34 | ``` 35 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-4/sum.exs: -------------------------------------------------------------------------------- 1 | defmodule Sum do 2 | def calculate(n) when is_integer(n) and n >= 0, do: do_calculate n 3 | def calculate(n) when not is_integer(n) or n < 0, do: {:error, "Expected a positive integer."} 4 | 5 | defp do_calculate(0), do: 0 6 | defp do_calculate(1), do: 1 7 | defp do_calculate(n), do: n + do_calculate(n - 1) 8 | end 9 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-5/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ModulesAndFunctions-5 2 | Write a function *gcd(x, y)* that finds the greatest common divisor between two nonnegative integers. Algebraically, *gcd(x, y)* is *x* if *y* is zero; it's *gcd(y, rem(x, y))* otherwise. 3 | 4 | ## Solution 5 | See the [gcd.exs](./gcd.exs) file for the full module. 6 | 7 | I'm not going to discuss the public function definitions since guard clauses aren't actually covered until the next section, but for the private functions that do the actual work, there are only two cases as described in the problem. 8 | 9 | Case 1: 10 | ``` 11 | defp do_calculate(x, 0), do: x 12 | ``` 13 | 14 | Case 2: 15 | ``` 16 | defp do_calculate(x, y), do: do_calculate(y, rem(x, y)) 17 | ``` 18 | 19 | Let's test it out: 20 | ``` 21 | iex> GCD.calculate(4, 12) 22 | 4 23 | iex> GCD.calculate(36, 48) 24 | 12 25 | ``` 26 | 27 | Dope. 28 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-5/gcd.exs: -------------------------------------------------------------------------------- 1 | defmodule GCD do 2 | def calculate(x, y) when is_integer(x) and is_integer(y) and x >= 0 and y >= 0, 3 | do: do_calculate(x, y) 4 | def calculate(x, y) when not is_integer(x) or not is_integer(y) or x < 0 and y < 0, 5 | do: {:error, "Expected two positive integers."} 6 | 7 | defp do_calculate(x, 0), do: x 8 | defp do_calculate(x, y), do: do_calculate(y, rem(x, y)) 9 | end 10 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-6/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ModulesAndFunctions-6 2 | *I'm thinking of a number between 1 and 1000...* 3 | 4 | The most efficient way to find the number is to guess halfway between the low and high numbers of the range. If our guess is too big, then the answer lies between the bottom of the range and one less than our guess. If our guess is too small, then the answer lies between one more than our guess and the end of the range. 5 | 6 | Your API will be *guess(actual, range)*, where *range* is an Elixir range. 7 | 8 | Your output should look similar to this: 9 | 10 | ``` 11 | iex> Chop.guess(273, 1..1000) 12 | Is it 500 13 | Is it 250 14 | Is it 375 15 | Is it 312 16 | Is it 281 17 | Is it 265 18 | Is it 273 19 | 273 20 | ``` 21 | 22 | ## Solution 23 | See the [chop.exs](./chop.exs) file for the full module. 24 | 25 | The first order of business is figuring out how to calculate our guess. It should be simple enough; we just need to get the middle point of the range. It turns out I couldn't find a way to do this built into Elixir, so I wrote myself a little helper function. 26 | ``` 27 | defp do_get_middle(%Range{} = range) do 28 | min..max = range 29 | count = max - min + 1 30 | max - div(count, 2) 31 | end 32 | ``` 33 | 34 | It will calculate the exact middle for a range containing an odd number of items, or the integer below the exact middle for ranges containing an even number of items (I.e. *do_get_middle(1..4) => 2*). 35 | 36 | Next is adding the public function definition to call our private helpers. I discovered that there is also no *is_range* method in Elixir but that a range is actually just a struct. So, we can use pattern matching in the function signature to ensure a range is passed: 37 | ``` 38 | def guess(n, %Range{} = range) when is_integer(n) do 39 | if not n in range do 40 | raise RuntimeError, message: "The integer provided is not in the range." 41 | end 42 | 43 | guess = do_get_middle range 44 | do_guess(n, range, guess) 45 | end 46 | ``` 47 | Unfortunately, there is no guard clause or pattern matching capable of verifying that the integer is in the range, so the first thing we should do is make sure it is and raise an exception if it's not. 48 | 49 | Then, we calculate our initial guess and call our worker methods. The worker methods are the fun part because we've already sanitized the input; now we just get to do the real work. There are only three cases we care about: 50 | - Our guess is correct 51 | - Our guess is too low 52 | - Our guess is too high 53 | 54 | We can check all three of these things using guard clauses! Here's our function signatures: 55 | ``` 56 | defp do_guess(n, _range, guess) when n == guess do 57 | # TODO 58 | end 59 | 60 | defp do_guess(n, range, guess) when n < guess do 61 | # TODO 62 | end 63 | 64 | defp do_guess(n, range, guess) when n > guess do 65 | # TODO 66 | end 67 | ``` 68 | 69 | Now, we just have a little bit of logic to add. The first case is easy; we found the number we are looking for so let's just print and return it. 70 | ``` 71 | defp do_guess(n, _range, guess) when n == guess do 72 | IO.puts "Is it #{guess}" 73 | guess 74 | end 75 | ``` 76 | 77 | The next two cases are nearly identical. In both cases all we need to do is adjust the range and make a new guess. If our guess was to low, we adjust the range so that it only contains values higher than our guess. Likewise, if our guess was too high, we adjust the range so that it only contains values lower than our guess. Then, in both cases, we just formulate a new guess and try again with the new range. 78 | ``` 79 | defp do_guess(n, range, guess) when n < guess do 80 | IO.puts "Is it #{guess}" 81 | 82 | min.._max = range 83 | new_max = guess - 1 84 | new_range = min..new_max 85 | new_guess = do_get_middle(new_range) 86 | do_guess(n, new_range, new_guess) 87 | end 88 | 89 | defp do_guess(n, range, guess) when n > guess do 90 | IO.puts "Is it #{guess}" 91 | 92 | _min..max = range 93 | new_min = guess + 1 94 | new_range = new_min..max 95 | new_guess = do_get_middle(new_range) 96 | do_guess(n, new_range, new_guess) 97 | end 98 | ``` 99 | 100 | The code could certainly be consolidated but I left it verbose to keep everything as clear as possible. Lastly, let's try it out: 101 | ``` 102 | iex> Chop.guess(273, 1..1000) 103 | Is it 500 104 | Is it 250 105 | Is it 375 106 | Is it 312 107 | Is it 281 108 | Is it 265 109 | Is it 273 110 | 273 111 | iex> Chop.guess(9,1..20) 112 | Is it 10 113 | Is it 5 114 | Is it 7 115 | Is it 8 116 | Is it 9 117 | 9 118 | iex> Chop.guess(2473, 1..10000) 119 | Is it 5000 120 | Is it 2500 121 | Is it 1250 122 | Is it 1875 123 | Is it 2187 124 | Is it 2343 125 | Is it 2421 126 | Is it 2460 127 | Is it 2480 128 | Is it 2470 129 | Is it 2475 130 | Is it 2472 131 | Is it 2473 132 | 2473 133 | ``` 134 | 135 | That's pretty neat! 136 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-6/chop.exs: -------------------------------------------------------------------------------- 1 | defmodule Chop do 2 | def guess(n, %Range{} = range) when is_integer(n) do 3 | if not n in range do 4 | raise RuntimeError, message: "The integer provided is not in the range." 5 | end 6 | 7 | guess = do_get_middle range 8 | do_guess(n, range, guess) 9 | end 10 | def guess(n, _range) when not is_integer(n), 11 | do: {:error, "Expected an integer and a range, where the integer lies within the range."} 12 | 13 | defp do_guess(n, _range, guess) when n == guess do 14 | IO.puts "Is it #{guess}" 15 | guess 16 | end 17 | 18 | defp do_guess(n, range, guess) when n < guess do 19 | IO.puts "Is it #{guess}" 20 | 21 | min.._max = range 22 | new_max = guess - 1 23 | new_range = min..new_max 24 | new_guess = do_get_middle(new_range) 25 | do_guess(n, new_range, new_guess) 26 | end 27 | 28 | defp do_guess(n, range, guess) when n > guess do 29 | IO.puts "Is it #{guess}" 30 | 31 | _min..max = range 32 | new_min = guess + 1 33 | new_range = new_min..max 34 | new_guess = do_get_middle(new_range) 35 | do_guess(n, new_range, new_guess) 36 | end 37 | 38 | # Return the middle of a range. 39 | defp do_get_middle(%Range{} = range) do 40 | min..max = range 41 | count = max - min + 1 42 | max - div(count, 2) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ch06/ModulesAndFunctions-7/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ModulesAndFunctions-7 2 | Find the library functions to do the following, and then use each in *iex*. (If the word *Elixir* or *Erlang* appears at the end of the challenge, then you'll find the answer in that set of libraries.) 3 | 4 | - Convert a float to a string with two decimal digits. (*Erlang*) 5 | - Get the value of an operating-system environment variable. (*Elixir*) 6 | - Return the extension component of a file name (so return .exs if given "dave/test.exs"). (*Elixir*) 7 | - Return the process's current working directory. (*Elixir*) 8 | - Convert a string containing JSON into Elixir's data structures. (Just find; don't install.) 9 | - Execute a command in your operating system's shell. 10 | 11 | ## Solutions 12 | 13 | ### Float Conversion 14 | The problem ends with *Erlang* so let's look at the Erlang docs. In *io* there is a method called format, but this alone doesn't look like what we want as it's coupled closely with *fwrite*. However, in *io_lib* I also see a *format* method which looks more promising; this one just returns a character list. Let's try it: 15 | ``` 16 | iex> print_float = fn n -> :io_lib.format("~.2f", [n]) end 17 | #Function<6.50752066/1 in :erl_eval.expr/5> 18 | iex> print_float.(2.123) 19 | ['2.12'] 20 | ``` 21 | 22 | Almost got it, except it returned a list of strings instead of just the string. I believe this is because the data parameter we passed in was a list. Anyways, let's just take the head: 23 | ``` 24 | iex> print_float = fn n -> 25 | ...> [head | _] = :io_lib.format("~.2f", [n]) 26 | ...> head 27 | ...> end 28 | #Function<6.50752066/1 in :erl_eval.expr/5> 29 | iex> print_float.(2.123) 30 | '2.12' 31 | iex> print_float.(273.1834) 32 | '273.18' 33 | ``` 34 | 35 | ### Environment Variable 36 | A quick Google search of "Elixir Environment Variable" brought me to the System module in the Elixir docs and *get_env* looks promising. Let's try it: 37 | ``` 38 | iex> System.get_env "SHELL" 39 | "/bin/bash" 40 | ``` 41 | 42 | ### File Extensions 43 | So I immediately look in the *File* and *File.Stat* modules thinking those were likely places for this, but after no success, a search in the docs led me to *Path*, where an *extname* function resides: 44 | ``` 45 | $ touch ~/test.txt 46 | $ iex 47 | iex> Path.extname("~/test.txt") 48 | ".txt" 49 | ``` 50 | 51 | ### Process CWD 52 | Again, I thought I new exactly where this one would be: *Process*. But, a quick search again led me to the right place: *File*. It has the function *cwd*: 53 | ``` 54 | iex> File.cwd 55 | {:ok, "/home/paul/dev/programming-elixir-exercises"} 56 | ``` 57 | 58 | ### JSON Deserialization 59 | Ironically, working on a side project I already did this using a library I found on Github called *Poison*. See it here: 60 | https://github.com/devinus/poison 61 | 62 | # OS Shell Execution 63 | Looking in the Elixir docs again, *System.cmd* looks like it should work: 64 | ``` 65 | iex> System.cmd "echo", ["Hello world"] 66 | {"Hello world\n", 0} 67 | ``` 68 | 69 | Righteous. 70 | -------------------------------------------------------------------------------- /ch06/times.exs: -------------------------------------------------------------------------------- 1 | defmodule Times do 2 | def double(n), do: n * 2 3 | 4 | def triple(n), do: n * 3 5 | 6 | def quadruple(n), do: n |> double |> double 7 | end 8 | -------------------------------------------------------------------------------- /ch07/ListsAndRecursion-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ListsAndRecursion-1 2 | Write a *mapsum* function that takes a list and a function. It applies the function to each element of the list and then sums the result, so: 3 | 4 | ``` 5 | iex> MyList.mapsum [1, 2, 3], &(&1 * &1) 6 | 14 7 | ``` 8 | 9 | ## Solution 10 | See the [my_list.exs](./my_list.exs) file for the full module. 11 | 12 | The name pretty much gives this one away. It can be split up into two steps: 13 | - Use Enum.map to apply the function on all elements. 14 | - Use Enum.sum to add up all of the results. 15 | 16 | Let's try writing it: 17 | ``` 18 | def mapsum(items, func) when is_list(items) and is_function(func) do 19 | items 20 | |> Enum.map(func) 21 | |> Enum.sum 22 | end 23 | ``` 24 | 25 | Now let's test it. 26 | ``` 27 | iex> MyList.mapsum [1, 2, 3], &(&1 * &1) 28 | 14 29 | iex> MyList.mapsum [1, 2, 3, 4], &(&1 + 2) 30 | 18 31 | ``` 32 | -------------------------------------------------------------------------------- /ch07/ListsAndRecursion-1/my_list.exs: -------------------------------------------------------------------------------- 1 | defmodule MyList do 2 | def mapsum(items, func) when is_list(items) and is_function(func) do 3 | items 4 | |> Enum.map(func) 5 | |> Enum.sum 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /ch07/ListsAndRecursion-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ListsAndRecursion-2 2 | Write a *max(list)* that returns the element with the maximum value in the list. (This is slightly trickier than it sounds.) 3 | 4 | ## Solution 5 | See the [my_list.exs](./my_list.exs) file for the full module. 6 | 7 | I'm guessing the "tricky" part is that we need to keep track of the current max. We'll use a private helper for this. We can do everything using pattern matching and guard clauses; we just need to consider the four possible cases: 8 | - We are looking at the first element in the list; no current max is held so set the element as the current max. 9 | - The element we are looking at is less than or equal to the current max; leave the current max alone and move on. 10 | - The element we are looking at is greater than the current max; update the current max and move on. 11 | - The list is empty. This is how we know we are done; return the current max. 12 | 13 | When we put it all together, it looks like: 14 | ``` 15 | defmodule MyList do 16 | def max(items) when is_list(items) do 17 | do_max(items) 18 | end 19 | 20 | defp do_max([h | t]), do: do_max(t, h) 21 | defp do_max([h | t], current_max) when h > current_max, do: do_max(t, h) 22 | defp do_max([h | t], current_max) when h <= current_max, do: do_max(t, current_max) 23 | defp do_max([], current_max), do: current_max 24 | end 25 | ``` 26 | 27 | This could be consolidated even more as we don't really need the first *do_max* definition; however, I chose to separate the two to make it more readable. 28 | 29 | In *iex*: 30 | ``` 31 | iex> MyList.max [1, 2, 3] 32 | 3 33 | iex> MyList.max [1, 2, 3, 2, 7, 4] 34 | 7 35 | ``` 36 | -------------------------------------------------------------------------------- /ch07/ListsAndRecursion-2/my_list.exs: -------------------------------------------------------------------------------- 1 | defmodule MyList do 2 | def max(items) when is_list(items) do 3 | do_max(items) 4 | end 5 | 6 | defp do_max([h | t]), do: do_max(t, h) 7 | defp do_max([h | t], current_max) when h > current_max, do: do_max(t, h) 8 | defp do_max([h | t], current_max) when h <= current_max, do: do_max(t, current_max) 9 | defp do_max([], current_max), do: current_max 10 | end 11 | -------------------------------------------------------------------------------- /ch07/ListsAndRecursion-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ListsAndRecursion-3 2 | An Elixir single-quoted string is actually a list of individual character codes. Write a *caesar(list, n)* function that adds *n* to each list element, wrapping if the addition results in a character greater than z. 3 | ``` 4 | iex> MyList.caesar('ryvkve', 13) 5 | ?????? :) 6 | ``` 7 | 8 | ## Solution 9 | See the [my_list.exs](./my_list.exs) file for the full module. 10 | 11 | The problem said wrap if greater than z, but we're going to make this work for both lower and upper case ASCII characters. So, there will be two cases we need to worry about: 12 | - Character is lower case (96 < char < 123) 13 | - Character is upper case (64 < char < 91) 14 | 15 | In both cases we will do the same thing; add the integer *n* to the character code and then either return the new character code, or the character code - 26 if it is greater than 123 or 91 respectively. 16 | 17 | ``` 18 | defmodule MyList do 19 | def caesar(char_list, n) when is_list(char_list) and is_integer(n) do 20 | char_list 21 | |> Enum.map(fn c -> do_obscure(c, n) end) 22 | end 23 | 24 | # Obscure upper case letter. 25 | defp do_obscure(char, n) when char > 64 and char < 91 do 26 | case char + n do 27 | new_char when new_char > 90 -> new_char - 26 28 | new_char when new_char < 91 -> new_char 29 | end 30 | end 31 | 32 | # Obscure lower case letter. 33 | defp do_obscure(char, n) when char > 96 and char < 123 do 34 | case char + n do 35 | new_char when new_char > 122 -> new_char - 26 36 | new_char when new_char < 123 -> new_char 37 | end 38 | end 39 | end 40 | ``` 41 | 42 | Simple enough, let's test it: 43 | ``` 44 | iex> MyList.caesar('abc', 2) 45 | 'cde' 46 | iex> MyList.caesar('Paul', 2) 47 | 'Rcwn' 48 | iex> MyList.caesar('xyz', 3) 49 | 'abc' 50 | ``` 51 | -------------------------------------------------------------------------------- /ch07/ListsAndRecursion-3/my_list.exs: -------------------------------------------------------------------------------- 1 | defmodule MyList do 2 | def caesar(char_list, n) when is_list(char_list) and is_integer(n) do 3 | char_list 4 | |> Enum.map(fn c -> do_obscure(c, n) end) 5 | end 6 | 7 | # Obscure upper case letter. 8 | defp do_obscure(char, n) when char > 64 and char < 91 do 9 | case char + n do 10 | new_char when new_char > 90 -> new_char - 26 11 | new_char when new_char < 91 -> new_char 12 | end 13 | end 14 | 15 | # Obscure lower case letter. 16 | defp do_obscure(char, n) when char > 96 and char < 123 do 17 | case char + n do 18 | new_char when new_char > 122 -> new_char - 26 19 | new_char when new_char < 123 -> new_char 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /ch07/ListsAndRecursion-4/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ListsAndRecursion-4 2 | Write a function *MyList.span(from, to)* that returns a list of the numbers from *from* up to *to*. 3 | 4 | ## Solution 5 | See the [my_list.exs](./my_list.exs) file for the full module. 6 | 7 | First off, there is a really easy way to implement this that I will do first: 8 | ``` 9 | def easy_span(from, to) when is_integer(from) and is_integer(to) and from < to do 10 | from..to 11 | |> Enum.to_list 12 | end 13 | ``` 14 | 15 | We create a range and then use the *Enum* module to convert it to a list. 16 | 17 | But, seeing as the point of the exercise is to practice lists and recursion, let's do it ourselves too. It's still pretty simple as there are only two cases we really care about: 18 | - We are not at the end of the list. 19 | - We are at the end of the list. 20 | 21 | Let's look at case 1: 22 | ``` 23 | defp do_span(current, last, acc) when current != last do 24 | do_span(current + 1, last, [current | acc]) 25 | end 26 | ``` 27 | We keep the accumulator handy instead so that we can take advantage of tail recursion optimization. We couldn't if we did something like this: 28 | ``` 29 | [current | do_span(current + 1, last)] 30 | ``` 31 | It would work but the call stack would build up. Anyways, all we are doing is adding the current number as the list's head and then recursively calling with the next integer. 32 | 33 | Notice that this will create the list in reverse order. This is purposeful; appending to the end of a list linear time whereas prepending is constant time. In the base case we will reverse the list which is a heavily optimized operation. 34 | 35 | Now case 2: 36 | ``` 37 | defp do_span(current, last, acc) when current == last do 38 | Enum.reverse [current | acc] 39 | end 40 | ``` 41 | 42 | This should look pretty similar. All we are doing is adding the final integer to the front of the list and then reversing the entire thing so it is returned in the correct order. Let's try both the easy implementation and the manual implementation in *iex*: 43 | ``` 44 | iex> MyList.easy_span(7, 14) 45 | [7, 8, 9, 10, 11, 12, 13, 14] 46 | iex> MyList.span(7, 14) 47 | [7, 8, 9, 10, 11, 12, 13, 14] 48 | ``` 49 | 50 | Looks good! 51 | -------------------------------------------------------------------------------- /ch07/ListsAndRecursion-4/my_list.exs: -------------------------------------------------------------------------------- 1 | defmodule MyList do 2 | def easy_span(from, to) when is_integer(from) and is_integer(to) and from < to do 3 | from..to 4 | |> Enum.to_list 5 | end 6 | 7 | def span(from, to) when is_integer(from) and is_integer(to) and from < to do 8 | do_span(from, to, []) 9 | end 10 | 11 | defp do_span(current, last, acc) when current != last do 12 | do_span(current + 1, last, [current | acc]) 13 | end 14 | defp do_span(current, last, acc) when current == last do 15 | Enum.reverse [current | acc] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /ch10/ListsAndRecursion-5/my_enum.exs: -------------------------------------------------------------------------------- 1 | defmodule MyEnum do 2 | def all?(list, condition) when is_list(list) and is_function(condition) do 3 | do_all(list, condition) 4 | end 5 | 6 | defp do_all([h | t], condition) do 7 | case condition.(h) do 8 | true -> do_all(t, condition) 9 | false -> false 10 | end 11 | end 12 | defp do_all([], _), do: true 13 | 14 | def each(list, fun) when is_list(list) and is_function(fun) do 15 | do_each(list, fun) 16 | end 17 | 18 | defp do_each([h | t], fun) do 19 | fun.(h) 20 | do_each(t, fun) 21 | end 22 | defp do_each([], _), do: :ok 23 | 24 | def filter(list, condition) when is_list(list) and is_function(condition) do 25 | do_filter(list, condition, []) 26 | end 27 | 28 | defp do_filter([h | t], condition, acc) do 29 | case condition.(h) do 30 | true -> do_filter(t, condition, [h | acc]) 31 | false -> do_filter(t, condition, acc) 32 | end 33 | end 34 | defp do_filter([], _, acc), do: Enum.reverse acc 35 | 36 | def split(list, split_on) when is_list(list) do 37 | do_split(list, split_on, []) 38 | end 39 | 40 | defp do_split([h | t], split_on, acc) do 41 | case h == split_on do 42 | true -> { Enum.reverse([h | acc]), t} 43 | false -> do_split t, split_on, [h | acc] 44 | end 45 | end 46 | defp do_split([], _split_on, acc), do: {Enum.reverse(acc), []} 47 | 48 | def take(list, n) when is_list(list) and is_integer(n) and n > 0, do: do_take(list, n, []) 49 | 50 | defp do_take([h | t], n, acc) when n > 0, do: do_take(t, n - 1, [ h | acc]) 51 | defp do_take(_, n, acc) when n == 0, do: Enum.reverse acc 52 | defp do_take([], _, acc), do: Enum.reverse acc 53 | end 54 | -------------------------------------------------------------------------------- /ch10/ListsAndRecursion-6/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ListsAndRecursion-6 2 | (Hard) Write a *flatten(list)* function that takes a list that may contain any number of sublists, which themselves may contain sublists, to any depth. It returns the elements of these lists as a flat list. 3 | 4 | ``` 5 | iex> MyList.flatten([1, [2, 3, [4]], 5, [[[6]]]]) 6 | [1, 2, 3, 4, 5, 6] 7 | ``` 8 | 9 | ## Solution 10 | See the [my_list.exs](./my_list.exs) file for the full module. 11 | 12 | Hard eh? Challenge accepted! Before we start coding, let's think about what' going to happen as we recursively iterate through the list. We may encounter: 13 | - An element (Let's assume integers for now) 14 | - A list 15 | 16 | In the case of an element, we should append it to an accumulator, which will be our top-level list. In the case of another list, we should recursively iterate through **that** list the same way we are iterating through the current list. First off, how are we going to differentiate between seeing a basic item and a list? Let's try using guard clauses: 17 | ``` 18 | def flatten(list) when is_list(list), do: do_flatten(list, []) 19 | 20 | defp do_flatten([h | t], acc) when not is_list(h) do 21 | # TODO 22 | end 23 | 24 | defp do_flatten([h | t], acc) when is_list(h) do 25 | # TODO 26 | end 27 | ``` 28 | 29 | So, the first *do_flatten* should only match when the element *h* is not also a list. The second implementation should only match when it *is* a list. adding in the logic we previously discussed: 30 | ``` 31 | def flatten(list) when is_list(list), do: do_flatten(list, []) 32 | 33 | defp do_flatten([h | t], acc) when not is_list(h), do: do_flatten(t, [h | acc]) 34 | defp do_flatten([h | t], acc) when is_list(h), do: do_flatten(h ++ t, acc) 35 | defp do_flatten([], acc), do: Enum.reverse acc 36 | ``` 37 | 38 | This looks a little weird; why are we concatenating the head and tail in that second *do_flatten* clause? Well, concatenation isn't optimal; however, this is the easiest way to make the function tail recursive. We could do something like this also: 39 | ``` 40 | defp do_flatten([h | t], acc) when not is_list(h), do: do_flatten(t, [h | acc]) 41 | defp do_flatten([h | t], acc) when is_list(h), do 42 | acc ++ do_flatten(h, acc) ++ do_flatten(t, acc) 43 | end 44 | defp do_flatten([], acc), do: Enum.reverse acc 45 | ``` 46 | 47 | But obviously that is even worse because wouldn't get the tail recursion optimization either. I'd have to put more thought into how to optimize this so we are only ever prepending one element and not doing concatenation, but for a quick solution it works: 48 | ``` 49 | iex> MyList.flatten [1, 2, [3, 4, [[5]]]] 50 | [1, 2, 3, 4, 5] 51 | iex> MyList.flatten [1, 2, [3, 4, [[[5]]]]] 52 | [1, 2, 3, 4, 5] 53 | ``` 54 | -------------------------------------------------------------------------------- /ch10/ListsAndRecursion-6/my_list.exs: -------------------------------------------------------------------------------- 1 | defmodule MyList do 2 | def flatten(list) when is_list(list), do: do_flatten(list, []) 3 | 4 | defp do_flatten([h | t], acc) when not is_list(h), do: do_flatten(t, [h | acc]) 5 | defp do_flatten([h | t], acc) when is_list(h), do: do_flatten(h ++ t, acc) 6 | defp do_flatten([], acc), do: Enum.reverse acc 7 | end 8 | -------------------------------------------------------------------------------- /ch10/ListsAndRecursion-7/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ListsAndRecursion-7 2 | In the last exercise of Chapter 7, *Lists and Recursion*, on page 61, you wrote a *span* function. Use it and list comprehensions to return a list of the prime numbers from 2 to *n*. 3 | 4 | ## Solution 5 | See the [prime_finder.exs](./prime_finder.exs) file for the full module. 6 | 7 | Recall the *easy_span* function we wrote. I am going to consolidate it a bit and use it again here: 8 | ``` 9 | def do_span(from, to) when is_integer(from) and is_integer(to) and from < to, 10 | do: from..to |> Enum.to_list 11 | ``` 12 | 13 | Now, how should we go about doing this? Here's my idea. Let's write a function that given an integer *n*, returns *true* or *false* depending on whether or not that number is prime. Then, we can use *Enum.filter* with our method to filter down the span. A number is prime if it is only divisible into a whole number by 1 and itself. 14 | ``` 15 | def is_prime(n) do 16 | case n do 17 | 1 -> false 18 | x when x < 4 -> true 19 | x when x == 5 or x == 7 -> true 20 | x when x == 4 or x == 6 -> false 21 | x -> do_span(2, div(x, 2) - 1) |> do_check_divs(x) 22 | end 23 | end 24 | 25 | defp do_check_divs([h | t], n) do 26 | case rem(n, h) == 0 do 27 | true -> false 28 | false -> do_check_divs(t, n) 29 | end 30 | end 31 | defp do_check_divs([], _n), do: true 32 | ``` 33 | 34 | Let's test this helper out first to make sure it works. We create a span of numbers from *2* to *(n / 2) - 1*; this is the span of numbers that are possible divisors. If *n* is a multiple of any of these, we know it's not prime. We have to evaluate 1 through 7 manually as the formula *(n / 2) - 1* will only work for numbers greater than 7. 35 | 36 | Now let's write the main function. 37 | ``` 38 | def do_find(n) do 39 | do_span(1, n) 40 | |> Enum.filter(&(do_is_prime(&1))) 41 | end 42 | ``` 43 | 44 | All we have to do is filter the list down to the numbers that are prime. Let's test it: 45 | ``` 46 | iex> PrimeFinder.find 4 47 | [1, 2, 3] 48 | iex> PrimeFinder.find 20 49 | [1, 2, 3, 5, 7, 11, 13, 17, 19] 50 | ``` 51 | 52 | Those look like primes to me! This is a horribly inefficient implementation. Even by just adding some basic short circuiting for numbers divisible by two and three we could probably increase the speed by an order of magnitude. Nevertheless, the problem was to create the list of primes from 2 to *n* and we accomplished that. Let's not worry about optimizing everything yet. 53 | -------------------------------------------------------------------------------- /ch10/ListsAndRecursion-7/prime_finder.exs: -------------------------------------------------------------------------------- 1 | defmodule PrimeFinder do 2 | require Integer 3 | 4 | def find(n) when is_integer(n) and n > 0, do: do_find(n) 5 | 6 | def do_find(n) do 7 | do_span(1, n) 8 | |> Enum.filter(&(do_is_prime(&1))) 9 | end 10 | 11 | def do_span(from, to) when is_integer(from) and is_integer(to) and from < to, 12 | do: from..to |> Enum.to_list 13 | 14 | def do_is_prime(n) do 15 | case n do 16 | 1 -> false 17 | x when x < 4 -> true 18 | x when x == 4 or x == 6 -> false 19 | x when x == 5 or x == 7 -> true 20 | x -> do_span(2, div(x, 2) - 1) |> do_check_divs(x) 21 | end 22 | end 23 | 24 | defp do_check_divs([h | t], n) do 25 | case rem(n, h) == 0 do 26 | true -> false 27 | false -> do_check_divs(t, n) 28 | end 29 | end 30 | defp do_check_divs([], _n), do: true 31 | end 32 | -------------------------------------------------------------------------------- /ch10/ListsAndRecursion-8/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ListsAndRecursion-8 2 | The Pragmatic Bookshelf has offices in Texas (TX) and North Carolina (NC), so we have to charge sales tax on orders shipped to these states. The rates can be expressed as a keyword list: 3 | ``` 4 | tax_rates = [NC: 0.075, TX: 0.08] 5 | ``` 6 | Here's a list of orders: 7 | ``` 8 | orders = [ 9 | [id: 123, ship_to: :NC, net_amount: 100.00], 10 | [id: 124, ship_to: :OK, net_amount: 35.50], 11 | [id: 125, ship_to: :TX, net_amount: 24.00], 12 | [id: 126, ship_to: :TX, net_amount: 44.00], 13 | [id: 127, ship_to: :NC, net_amount: 25.00], 14 | [id: 128, ship_to: :MA, net_amount: 10.00], 15 | [id: 129, ship_to: :CA, net_amount: 102.00], 16 | [id: 120, ship_to: :NC, net_amount: 50.00] 17 | ] 18 | ``` 19 | Write a function that takes both lists and returns a copy of the orders, but with an extra field, total_amount, which is the net plus sales tax. If a shipment is not to NC or TX, there's no tax applied. 20 | 21 | ## Solution 22 | See the [price_calculator.exs](./price_calculator.exs) file for the full module. 23 | 24 | Where do we start? Well, we know that we are going to take the collection of orders, apply a function to each order, and return a new collection which is the result of each operation. That's an immediate hint that we will probably want to use *Enum.map*. So, the next question is, what do we do to each order? 25 | 26 | For each order, we are going to look at the value of *:ship_to*, and then we will cross-reference it with the value in the tax rates. If a tax rate exists for the state, we will apply it, otherwise we will just set the total amount to the net amount. Let's write a helper function called *do_apply_tax* to make our lives easier. Given an order, it will check for a tax rate and then return the order with the total_amount field. 27 | ``` 28 | defp do_apply_tax(tax_rates, order) do 29 | tax = Keyword.get(tax_rates, order[:ship_to], 0) 30 | Keyword.put(order, :total_amount, order[:net_amount] + order[:net_amount] * tax) 31 | end 32 | ``` 33 | 34 | The *Keyword.get* function gives us the option of specifying a default value if the key is not found, so anything other than *:NC* and *:TX* will simply return a tax rate of 0. Then, we return the order with the new field added. 35 | 36 | Now, let's run it accross all of the orders: 37 | ``` 38 | def calculate(tax_rates, orders) do 39 | orders 40 | |> Enum.map(fn order -> do_apply_tax(order, tax_rates) end) 41 | end 42 | ``` 43 | 44 | Let's test it: 45 | ``` 46 | iex> tax_rates 47 | [NC: 0.075, TX: 0.08] 48 | iex> orders 49 | [[id: 123, ship_to: :NC, net_amount: 100.0], 50 | [id: 124, ship_to: :OK, net_amount: 35.5], 51 | [id: 125, ship_to: :TX, net_amount: 24.0], 52 | [id: 126, ship_to: :TX, net_amount: 44.0], 53 | [id: 127, ship_to: :NC, net_amount: 25.0], 54 | [id: 128, ship_to: :MA, net_amount: 10.0], 55 | [id: 129, ship_to: :CA, net_amount: 102.0], 56 | [id: 120, ship_to: :NC, net_amount: 50.0]] 57 | iex> PriceCalculator.calculate(tax_rates, orders) 58 | [[total_amount: 107.5, id: 123, ship_to: :NC, net_amount: 100.0], 59 | [total_amount: 35.5, id: 124, ship_to: :OK, net_amount: 35.5], 60 | [total_amount: 25.92, id: 125, ship_to: :TX, net_amount: 24.0], 61 | [total_amount: 47.52, id: 126, ship_to: :TX, net_amount: 44.0], 62 | [total_amount: 26.875, id: 127, ship_to: :NC, net_amount: 25.0], 63 | [total_amount: 10.0, id: 128, ship_to: :MA, net_amount: 10.0], 64 | [total_amount: 102.0, id: 129, ship_to: :CA, net_amount: 102.0], 65 | [total_amount: 53.75, id: 120, ship_to: :NC, net_amount: 50.0]] 66 | ``` 67 | 68 | Wow. 69 | -------------------------------------------------------------------------------- /ch10/ListsAndRecursion-8/price_calculator.exs: -------------------------------------------------------------------------------- 1 | defmodule PriceCalculator do 2 | def calculate(tax_rates, orders) do 3 | orders 4 | |> Enum.map(fn order -> do_apply_tax(order, tax_rates) end) 5 | end 6 | 7 | def do_apply_tax(order, tax_rates) do 8 | tax = Keyword.get(tax_rates, order[:ship_to], 0) 9 | Keyword.put(order, :total_amount, order[:net_amount] + order[:net_amount] * tax) 10 | end 11 | 12 | def orders do 13 | [ 14 | [id: 123, ship_to: :NC, net_amount: 100.00], 15 | [id: 124, ship_to: :OK, net_amount: 35.50], 16 | [id: 125, ship_to: :TX, net_amount: 24.00], 17 | [id: 126, ship_to: :TX, net_amount: 44.00], 18 | [id: 127, ship_to: :NC, net_amount: 25.00], 19 | [id: 128, ship_to: :MA, net_amount: 10.00], 20 | [id: 129, ship_to: :CA, net_amount: 102.00], 21 | [id: 120, ship_to: :NC, net_amount: 50.00] 22 | ] 23 | end 24 | 25 | def tax_rates do 26 | [NC: 0.075, TX: 0.08] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: StringsAndBinaries-1 2 | Write a function that returns *true* if a single-quoted string contains only printable ASCII characters (space through tilde). 3 | 4 | ## Solution 5 | See the [my_string.exs](./my_string.exs) file for the full module. 6 | 7 | Recall that a single-quoted string is just a list of character codes. So, we can recursively iterate through each character and check whether or not the character is printable. We can even accomplish this using guard clauses to make our function implementations extremely simple. 8 | ``` 9 | def printable?(string) do 10 | do_printable(string) 11 | end 12 | 13 | defp do_printable([h | t]) when h > 31 and h < 127, do: do_printable(t) 14 | defp do_printable([h | _t]) when h < 32 or h > 126, do: false 15 | defp do_printable([]), do: true 16 | ``` 17 | 18 | In *iex*: 19 | ``` 20 | iex> MyString.printable? 'Hello' 21 | true 22 | iex> MyString.printable? [0, 2] 23 | false 24 | ``` 25 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-1/my_string.exs: -------------------------------------------------------------------------------- 1 | defmodule MyString do 2 | def printable?(string) do 3 | do_printable(string) 4 | end 5 | 6 | defp do_printable([h | t]) when h > 31 and h < 127, do: do_printable(t) 7 | defp do_printable([h | _t]) when h < 32 or h > 126, do: false 8 | defp do_printable([]), do: true 9 | end 10 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: StringsAndBinaries-2 2 | Write an *anagram?(word1, word2)* that returns *true* if its parameters are anagrams. 3 | 4 | ## Solution 5 | See the [my_string.exs](./my_string.exs) file for the full module. 6 | 7 | The problem doesn't specify if the parameters should be strings or char lists. I'll implement it for both and the mixed cases since the logic itself is only one line. 8 | ``` 9 | def anagram?(word1, word2) when is_list(word1) and is_list(word2), 10 | do: anagram?(List.to_string(word1), List.to_string(word2)) 11 | def anagram?(word1, word2) when is_list(word1) and is_binary(word2), 12 | do: anagram?(List.to_string(word1), word2) 13 | def anagram?(word1, word2) when is_binary(word1) and is_list(word2), 14 | do: anagram?(word1, List.to_string(word2)) 15 | def anagram?(word1, word2) when is_binary(word1) and is_binary(word2), 16 | do: word1 == String.reverse word2 17 | ``` 18 | 19 | There are four cases here but note that only the last one does the actual check. The first three simply convert any character lists into binaries and then call the function again. When it is called again, the last clause should then match and the check should be done. 20 | 21 | The check itself is simple, if the first word is equal to the reverse of the second word, the words are anagrams. 22 | 23 | In *iex*: 24 | ``` 25 | iex> MyString.anagram? "Paul", "luaP" 26 | true 27 | iex> MyString.anagram? "Paul", 'luaP' 28 | true 29 | iex> MyString.anagram? "Paul", "paul" 30 | false 31 | ``` 32 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-2/my_string.exs: -------------------------------------------------------------------------------- 1 | defmodule MyString do 2 | def anagram?(word1, word2) when is_list(word1) and is_list(word2), 3 | do: anagram?(List.to_string(word1), List.to_string(word2)) 4 | def anagram?(word1, word2) when is_list(word1) and is_binary(word2), 5 | do: anagram?(List.to_string(word1), word2) 6 | def anagram?(word1, word2) when is_binary(word1) and is_list(word2), 7 | do: anagram?(word1, List.to_string(word2)) 8 | def anagram?(word1, word2) when is_binary(word1) and is_binary(word2), 9 | do: word1 == String.reverse word2 10 | end 11 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: StringsAndBinaries-3 2 | Try the following in iex: 3 | ``` 4 | iex> ['cat' | 'dog'] 5 | ['cat', 100, 111, 103] 6 | ``` 7 | 8 | ## Solution 9 | When using the `[head | tail]` operator, *head* is a single element whereas *tail* is the list of remaining elements. So, when we say ['ab' | 'cd'], the first *element* of the list becomes the sublist 'ab'. This is the same as saying: 10 | ``` 11 | [[99, 97, 116], 100, 111, 103] 12 | ``` 13 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-4/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: StringsAndBinaries-4 2 | (Hard) Write a function that takes a single-quoted string of the form *number* [+-*/] *number* and returns the result of the calculation. The individual numbers do not have leading plus or minus signs. 3 | 4 | ## Solution 5 | See the [calculator.exs](./calculator.exs) file for the full module. 6 | 7 | The first order of business is to split up the three tokens. We can do this pretty easily by converted the char list to a string and using the *String.split* function on a space. Once we have the three tokens, we should use the Float module to parse the first and third token into a float. 8 | 9 | Finally, once we have the float representation of the numbers we are calculating, we can simply check which operation was passed in an apply it to the two numbers. 10 | 11 | ``` 12 | def calculate(equation) when is_list(equation) do 13 | [num1_string, op_string, num2_string] = 14 | List.to_string(equation) 15 | |> String.split(" ") 16 | 17 | {num1, _} = Float.parse(num1_string) 18 | {num2, _} = Float.parse(num2_string) 19 | case op_string do 20 | "+" -> num1 + num2 21 | "-" -> num1 - num2 22 | "*" -> num1 * num2 23 | "/" -> num1 / num2 24 | end 25 | end 26 | ``` 27 | 28 | That wasn't so bad! Let's test that it works: 29 | ``` 30 | iex> Calculator.calculate '12 + 3' 31 | 15.0 32 | iex> Calculator.calculate '14.5 - 12' 33 | 2.5 34 | iex> Calculator.calculate '1623 * 0.5' 35 | 811.5 36 | iex> Calculator.calculate '36 / 12' 37 | 3.0 38 | ``` 39 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-4/calculator.exs: -------------------------------------------------------------------------------- 1 | defmodule Calculator do 2 | def calculate(equation) when is_list(equation) do 3 | [num1_string, op_string, num2_string] = 4 | List.to_string(equation) 5 | |> String.split(" ") 6 | 7 | {num1, _} = Float.parse(num1_string) 8 | {num2, _} = Float.parse(num2_string) 9 | case op_string do 10 | "+" -> num1 + num2 11 | "-" -> num1 - num2 12 | "*" -> num1 * num2 13 | "/" -> num1 / num2 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-5/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: StringsAndBinaries-5 2 | Write a function that takes a list of dqs and prints each on a separate line, centered in a column that has the width of the longest string. Make sure it works with UTF characters. 3 | ``` 4 | iex> center(["cat", "zebra", "elephant"]) 5 | cat 6 | zebra 7 | elephant 8 | ``` 9 | 10 | ## Solution 11 | See the [strings.exs](./strings.exs) file for the full module. 12 | 13 | First we should get the length of the longest string. We can do this pretty easily using the Enum.max_by function to get the longest string and then taking its length. 14 | ``` 15 | width = Enum.max_by(strings, &(String.length(&1))) 16 | |> String.length 17 | ``` 18 | 19 | Next we will want to call our pretty printing helper function on each string like so: 20 | ``` 21 | Enum.each(strings, fn string -> do_print(string, width) end) 22 | ``` 23 | 24 | Now, let's do the actual work. We should get the difference in length between the string and the total width, then we will want to pad the left side by half of the difference. We can easily create the padding string using String.duplicate to create as many spaces as we need, and finally we can print it using string interpolation. 25 | ``` 26 | defp do_print(string, width) do 27 | dlength = width - String.length(string) 28 | padding = String.duplicate(" ", div(dlength, 2)) 29 | IO.puts "#{padding}#{string}" 30 | end 31 | ``` 32 | 33 | Let's test it: 34 | ``` 35 | iex> Strings.center(["cat", "zebra", "elephant"]) 36 | cat 37 | zebra 38 | elephant 39 | :ok 40 | ``` 41 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-5/strings.exs: -------------------------------------------------------------------------------- 1 | defmodule Strings do 2 | def center(strings) do 3 | width = Enum.max_by(strings, &(String.length(&1))) 4 | |> String.length 5 | Enum.each(strings, fn string -> do_print(string, width) end) 6 | end 7 | 8 | defp do_print(string, width) do 9 | dlength = width - String.length(string) 10 | padding = String.duplicate(" ", div(dlength, 2)) 11 | IO.puts "#{padding}#{string}" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-6/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: StringsAndBinaries-6 2 | Write a function to capitalize the sentences in a string. Each sentence is terminated by a period and a string. Right now, the case of the characters in the string is random. 3 | 4 | ## Solution 5 | See the [sentences.exs](./sentences.exs) file for the full module. 6 | 7 | They gave us a pretty big hint by telling us each sentence is terminated by a period and a string; now we know what we can split on. Let's start there: 8 | ``` 9 | defp do_separate_sentences(paragraph) do 10 | sentences = String.split(paragraph, ". ") 11 | end 12 | ``` 13 | 14 | The only problem with this is that by splitting on ". ", we lose those characters so we need to append a period to all but the last sentence. 15 | ``` 16 | defp do_separate_sentences(paragraph) do 17 | sentences = String.split(paragraph, ". ") 18 | Enum.map sentences, fn sentence -> 19 | case String.ends_with?(sentence, ".") do 20 | false -> sentence <> "." 21 | true -> sentence 22 | end 23 | end 24 | end 25 | ``` 26 | Okay, now we have separated out the sentences so we can move on to capitalization. The *String* module provides this for us so all we need to do is: 27 | ``` 28 | defp do_capitalize(sentences) do 29 | Enum.map sentences, fn sentence -> String.capitalize sentence end 30 | end 31 | ``` 32 | And finally, we need to bring the sentences back together again: 33 | ``` 34 | defp do_join_sentences(sentences) do 35 | Enum.join sentences, " " 36 | end 37 | ``` 38 | 39 | Put it all together: 40 | ``` 41 | def capitalize(paragraph) do 42 | paragraph 43 | |> do_separate_sentences 44 | |> do_capitalize 45 | |> do_join_sentences 46 | end 47 | ``` 48 | 49 | Let's test it: 50 | ``` 51 | iex> Sentences.capitalize "oh. a DOG. woof." 52 | "Oh. A dog. Woof." 53 | ``` 54 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-6/sentences.exs: -------------------------------------------------------------------------------- 1 | defmodule Sentences do 2 | def capitalize(paragraph) do 3 | paragraph 4 | |> do_separate_sentences 5 | |> do_capitalize 6 | |> do_join_sentences 7 | end 8 | 9 | defp do_separate_sentences(paragraph) do 10 | sentences = String.split(paragraph, ". ") 11 | Enum.map sentences, fn sentence -> 12 | case String.ends_with?(sentence, ".") do 13 | false -> sentence <> "." 14 | true -> sentence 15 | end 16 | end 17 | end 18 | 19 | defp do_capitalize(sentences) do 20 | Enum.map sentences, fn sentence -> String.capitalize sentence end 21 | end 22 | 23 | defp do_join_sentences(sentences) do 24 | Enum.join sentences, " " 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-7/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: StringsAndBinaries-7 2 | Chapter 7, *Lists and Recursion*, on page 61, had an exercise about calculating sales tax on page 104. We now have the sales information in a file of comma-separated *id*, *ship_to*, and *amount* values. The file looks like this: 3 | 4 | ``` 5 | id,ship_to,net_amount 6 | 123,:NC,100.00 7 | 124,:OK,35.50 8 | 125,:TX,24.00 9 | 126,:TX,44.80 10 | 127,:NC,25.00 11 | 128,:MA,10.00 12 | 129,:CA,102.00 13 | 120,:NC,50.00 14 | ``` 15 | 16 | Write a function that reads and parses this file and then passes the result to the *sales_tax* function. Remember that the data should be formatted into a keyword list, and that the fields need to be the correct types (so the *id* field is an integer and so on). 17 | 18 | You'll need the library functions *File.open*, *IO.read(file, :line)*, and *IO.stream(file)*. 19 | 20 | ## Solution 21 | See the [price_calculator.exs](./price_calculator.exs) file for the full module. 22 | 23 | Let's start off by reading the file and parsing the orders. I believe the problem is a little dated as the *IO.read* function isn't actually necessary anymore and there is no *IO.stream/1* function. Nevertheless, after looking at the Elixir docs I was able to figure out that pretty much everything we need in order to read the file we can get from *File.open!* and *IO.stream/2*. 24 | 25 | So, we want to open the CSV file, read each line, and strip off any leading/trailing white space. Notice that we also separate out the *header* line and ignore it since it doesn't contain any data we care about. Lastly, we take all of the lines and run another function *do_parse_order* on each one to transform each line into the data structure we want. 26 | ``` 27 | defp do_read_orders do 28 | file = File.open! "orders.csv" 29 | 30 | # Note that in Elixir 1.3 String.strip was renamed to String.trim 31 | [_header | lines] = Enum.map IO.stream(file, :line), &String.strip(&1) 32 | 33 | Enum.map lines, &do_parse_order(&1) 34 | end 35 | ``` 36 | 37 | Given the file is in CSV format, we can break up the tokens by splitting the string on a comma. This gives us each of the string representations that then need to be converted to their correct types. The ID and net amount are simple since we can just use parse; however, there is a bit of a curve ball with the *ship_to* field because the *Atom.parse* method doesn't expect the colon character to be present. If it is, *Atom.parse* won't return what you expect: `Atom.parse ":ok" #=> :":ok"`. So, we remove the colon. Lastly, we create the *Keyword list* and return it. 38 | ``` 39 | defp do_parse_order(order) do 40 | [id_string, ship_to_string, net_amount_string] = String.split(order, ",") 41 | {id, _} = Integer.parse id_string 42 | ship_to = String.replace(ship_to_string, ":", "") |> String.to_atom 43 | {net_amount, _} = Float.parse net_amount_string 44 | Keyword.new([{:id, id}, {:ship_to, ship_to}, {:net_amount, net_amount}]) 45 | end 46 | ``` 47 | 48 | Now that we've parsed the file and have the list of orders in the expected format, we can just run the method we already wrote in the previous exercise: 49 | ``` 50 | def calculate do 51 | do_read_orders 52 | |> Enum.map(fn order -> do_apply_tax(order, tax_rates) end) 53 | end 54 | 55 | # This was already written previously. 56 | defp do_apply_tax(order, tax_rates) do 57 | tax = Keyword.get(tax_rates, order[:ship_to], 0) 58 | Keyword.put(order, :total_amount, order[:net_amount] + order[:net_amount] * tax) 59 | end 60 | 61 | def tax_rates do 62 | [NC: 0.075, TX: 0.08] 63 | end 64 | ``` 65 | 66 | Let's test it: 67 | ``` 68 | iex> PriceCalculator.calculate 69 | [[total_amount: 107.5, id: 123, ship_to: :NC, net_amount: 100.0], 70 | [total_amount: 35.5, id: 124, ship_to: :OK, net_amount: 35.5], 71 | [total_amount: 25.92, id: 125, ship_to: :TX, net_amount: 24.0], 72 | [total_amount: 48.384, id: 126, ship_to: :TX, net_amount: 44.8], 73 | [total_amount: 26.875, id: 127, ship_to: :NC, net_amount: 25.0], 74 | [total_amount: 10.0, id: 128, ship_to: :MA, net_amount: 10.0], 75 | [total_amount: 102.0, id: 129, ship_to: :CA, net_amount: 102.0], 76 | [total_amount: 53.75, id: 120, ship_to: :NC, net_amount: 50.0]] 77 | ``` 78 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-7/orders.csv: -------------------------------------------------------------------------------- 1 | id,ship_to,net_amount 2 | 123,:NC,100.00 3 | 124,:OK,35.50 4 | 125,:TX,24.00 5 | 126,:TX,44.80 6 | 127,:NC,25.00 7 | 128,:MA,10.00 8 | 129,:CA,102.00 9 | 120,:NC,50.00 10 | -------------------------------------------------------------------------------- /ch11/StringsAndBinaries-7/price_calculator.exs: -------------------------------------------------------------------------------- 1 | defmodule PriceCalculator do 2 | def calculate do 3 | do_read_orders 4 | |> Enum.map(fn order -> do_apply_tax(order, tax_rates) end) 5 | end 6 | 7 | defp do_read_orders do 8 | file = File.open! "orders.csv" 9 | 10 | # Note that in Elixir 1.3 String.strip was renamed to String.trim 11 | [_header | lines] = Enum.map IO.stream(file, :line), &String.strip(&1) 12 | 13 | Enum.map lines, &do_parse_order(&1) 14 | end 15 | 16 | defp do_parse_order(order) do 17 | [id_string, ship_to_string, net_amount_string] = String.split(order, ",") 18 | {id, _} = Integer.parse id_string 19 | ship_to = String.replace(ship_to_string, ":", "") |> String.to_atom 20 | {net_amount, _} = Float.parse net_amount_string 21 | Keyword.new([{:id, id}, {:ship_to, ship_to}, {:net_amount, net_amount}]) 22 | end 23 | 24 | defp do_apply_tax(order, tax_rates) do 25 | tax = Keyword.get(tax_rates, order[:ship_to], 0) 26 | Keyword.put(order, :total_amount, order[:net_amount] + order[:net_amount] * tax) 27 | end 28 | 29 | def tax_rates do 30 | [NC: 0.075, TX: 0.08] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /ch12/ControlFlow-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ControlFlow-1 2 | Rewrite the FizzBuzz example using case. 3 | 4 | ## Solution 5 | See the [fizzbuzz.exs](./fizzbuzz.exs) file for the full module. 6 | 7 | Recall our previous solution to fizz buzz: 8 | ``` 9 | fizz_buzz_helper = fn 10 | 0, 0, c -> "FizzBuzz" 11 | 0, b, c -> "Fizz" 12 | a, 0, c -> "Buzz" 13 | a, b, c -> c 14 | end 15 | 16 | fizz_buzz = fn n -> fizz_buzz_helper.(rem(n, 3), rem(n,5), n) end 17 | ``` 18 | 19 | Looks easy enough to convert. Let's write the public method: 20 | ``` 21 | def calculate(n) do 22 | do_fizz_buzz(rem(n, 3), rem(n,5), n) 23 | end 24 | ``` 25 | 26 | Which will call a private helper: 27 | ``` 28 | defp do_fizz_buzz(a, b, c) do 29 | case {a, b, c} do 30 | {0, 0, _c} -> "FizzBuzz" 31 | {0, _b, _c} -> "Fizz" 32 | {_a, 0, _c} -> "Buzz" 33 | {_a, _b, _c} -> c 34 | end 35 | end 36 | ``` 37 | 38 | The only difference is that in the *case* statement we wrap the values in a tuple as a single condition is expected. 39 | 40 | In *iex*: 41 | ``` 42 | iex> FizzBuzz.calculate(5) 43 | "Buzz" 44 | iex> 1..15 |> Enum.to_list |> Enum.map(&FizzBuzz.calculate(&1)) 45 | [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, 46 | "FizzBuzz"] 47 | ``` 48 | -------------------------------------------------------------------------------- /ch12/ControlFlow-1/fizzbuzz.exs: -------------------------------------------------------------------------------- 1 | defmodule FizzBuzz do 2 | def calculate(n) do 3 | do_fizz_buzz(rem(n, 3), rem(n,5), n) 4 | end 5 | 6 | defp do_fizz_buzz(a, b, c) do 7 | case {a, b, c} do 8 | {0, 0, _c} -> "FizzBuzz" 9 | {0, _b, _c} -> "Fizz" 10 | {_a, 0, _c} -> "Buzz" 11 | {_a, _b, _c} -> c 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /ch12/ControlFlow-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ControlFlow-2 2 | We now have three different implementations of FizzBuzz. One uses *cond*, one uses *case*, and one uses separate functions with guard clauses. 3 | 4 | Take a minute to look at all three. Which do you feel best expresses the problem? Which will be easiest to maintain? 5 | 6 | The *case* style and the implementation using guard clauses are different from control structures in most other languages. If you feel that one of these was the best implementation, can you think of ways to remind yourself to investigate these options as you write Elixir code in the future? 7 | 8 | ## Solution 9 | I definitely think that the implementation using separate functions and guard clauses feels the cleanest and easiest to maintain. It would be trivial to modify one of the cases without even touching any of the others' implementations. 10 | 11 | I don't think I will need a way to remember; it always to to use mechanisms that make the most sense for the task I am trying to accomplish. Granted I am more used to OOP control flow, I will probably just always consider guard clauses first and ask myself if they make sense. In the off chance that they make the task more complicated, then perhaps it makes sense to go with something else. 12 | -------------------------------------------------------------------------------- /ch12/ControlFlow-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: ControlFlow-3 2 | Many built-in functions have two forms. The *xxx* form returns the tuple *{:ok, data}* and the *xxx!* form returns data on success but raises an exception otherwise. However, some functions don't have the *xxx!* form. 3 | 4 | Write an *ok!* function that takes an arbitrary parameter. If the parameter is the tuple *{:ok, data}*, return the data. Otherwise, raise an exception containing information from the parameter. 5 | 6 | You could use your function like this: 7 | ``` 8 | file = ok! File.open("somefile") 9 | ``` 10 | 11 | ## Solution 12 | I'm not going to bother putting this in a module. Let's just do this with an anonymous function. It can use pattern matching to identify whether or not *{:ok, data}* was passed in. 13 | ``` 14 | ok! = fn 15 | {:ok, data} -> data 16 | _ -> raise RuntimeError 17 | end 18 | ``` 19 | 20 | In *iex*: 21 | ``` 22 | $ touch exists.txt 23 | $ iex 24 | iex> ok! = fn 25 | ...> {:ok, data} -> data 26 | ...> _ -> raise RuntimeError 27 | ...> end 28 | #Function<6.50752066/1 in :erl_eval.expr/5> 29 | iex> ok!.(File.open("exists.txt")) 30 | #PID<0.64.0> 31 | iex> ok!.(File.open("doesnt_exist.txt")) 32 | ** (RuntimeError) runtime error 33 | ``` 34 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OrganizingAProject-1 2 | Do what I did. Honest. Create the project and write and test the option parser. It's one thing to read about it, but you'll be doing this a lot, so you may as well start now. 3 | 4 | ## Solution 5 | See the [issues](./issues) directory for the mix project containing the solution. 6 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-1/issues/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-1/issues/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-1/issues/lib/issues.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues do 2 | end 3 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-1/issues/lib/issues/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues.CLI do 2 | @default_count 4 3 | 4 | @moduledoc """ 5 | Handle the command line parsing and the dispatch to the various functions that 6 | end up generating a table of the last _n_ issues in a github project. 7 | """ 8 | 9 | def run(argv) do 10 | parse_args(argv) 11 | end 12 | 13 | @doc """ 14 | `argv` can be -h or --help, which returns :help. 15 | 16 | Otherwise it is a github user name, project name, and (optionally) the number 17 | of entries to format. 18 | 19 | Return a tuple of `{ user, project, count }`, or `:help` if help was given. 20 | """ 21 | def parse_args(argv) do 22 | parse = OptionParser.parse(argv, switches: [ help: :boolean], aliases: [ h: :help]) 23 | 24 | case parse do 25 | { [ help: true ], _, _ } -> :help 26 | 27 | { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) } 28 | 29 | { _, [ user, project ], _ } -> { user, project, @default_count } 30 | 31 | _ -> :help 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-1/issues/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Issues.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :issues, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger]] 15 | end 16 | 17 | defp deps do 18 | [] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-1/issues/test/cli_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliTest do 2 | use ExUnit.Case 3 | 4 | import Issues.CLI, only: [ parse_args: 1 ] 5 | 6 | test ":help returned by option parsing with -h and --help options" do 7 | assert parse_args(["-h", "anything"]) == :help 8 | assert parse_args(["-help", "anything"]) == :help 9 | end 10 | 11 | test "three values returned if three given" do 12 | assert parse_args(["user", "project", "99"]) == { "user", "project", 99} 13 | end 14 | 15 | test "count is defaulted if two values given" do 16 | assert parse_args(["user", "project"]) == { "user", "project", 4} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-1/issues/test/issues_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IssuesTest do 2 | use ExUnit.Case 3 | doctest Issues 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-1/issues/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OrganizingAProject-2 2 | Add the dependency to your project and install it. 3 | 4 | ## Solution 5 | See the [issues](./issues) directory for the mix project containing the solution. 6 | 7 | This change specifically was adding the following dependency to the *mix.exs* file: 8 | ``` 9 | {:httpoison, "~> 0.8.3"} 10 | ``` 11 | 12 | We then install the dependency by running: 13 | ``` 14 | mix deps.get 15 | ``` 16 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-2/issues/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-2/issues/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-2/issues/lib/issues.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues do 2 | end 3 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-2/issues/lib/issues/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues.CLI do 2 | @default_count 4 3 | 4 | @moduledoc """ 5 | Handle the command line parsing and the dispatch to the various functions that 6 | end up generating a table of the last _n_ issues in a github project. 7 | """ 8 | 9 | def run(argv) do 10 | parse_args(argv) 11 | end 12 | 13 | @doc """ 14 | `argv` can be -h or --help, which returns :help. 15 | 16 | Otherwise it is a github user name, project name, and (optionally) the number 17 | of entries to format. 18 | 19 | Return a tuple of `{ user, project, count }`, or `:help` if help was given. 20 | """ 21 | def parse_args(argv) do 22 | parse = OptionParser.parse(argv, switches: [ help: :boolean], aliases: [ h: :help]) 23 | 24 | case parse do 25 | { [ help: true ], _, _ } -> :help 26 | 27 | { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) } 28 | 29 | { _, [ user, project ], _ } -> { user, project, @default_count } 30 | 31 | _ -> :help 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-2/issues/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Issues.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :issues, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger]] 15 | end 16 | 17 | defp deps do 18 | [ 19 | {:httpoison, "~> 0.8.3"} 20 | ] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-2/issues/test/cli_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliTest do 2 | use ExUnit.Case 3 | 4 | import Issues.CLI, only: [ parse_args: 1 ] 5 | 6 | test ":help returned by option parsing with -h and --help options" do 7 | assert parse_args(["-h", "anything"]) == :help 8 | assert parse_args(["-help", "anything"]) == :help 9 | end 10 | 11 | test "three values returned if three given" do 12 | assert parse_args(["user", "project", "99"]) == { "user", "project", 99} 13 | end 14 | 15 | test "count is defaulted if two values given" do 16 | assert parse_args(["user", "project"]) == { "user", "project", 4} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-2/issues/test/issues_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IssuesTest do 2 | use ExUnit.Case 3 | doctest Issues 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-2/issues/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OrganizingAProject-3 2 | Bring your version of this project in line with the code here. 3 | 4 | ## Solution 5 | See the [issues](./issues) directory for the mix project containing the solution. 6 | 7 | I'm not going to explain much as we're pretty much just filling in what is laid out in the chapter. One notable difference is the definition of the `Issues.GithubIssues.handle_response` method. We actually need to match on tuple; I'm guessing this was changed in a later version of *HTTPoison*. 8 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | config :issues, github_url: "https://api.github.com" 3 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/lib/issues.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues do 2 | end 3 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/lib/issues/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues.CLI do 2 | @default_count 4 3 | 4 | @moduledoc """ 5 | Handle the command line parsing and the dispatch to the various functions that 6 | end up generating a table of the last _n_ issues in a github project. 7 | """ 8 | 9 | def run(argv) do 10 | parse_args(argv) 11 | end 12 | 13 | @doc """ 14 | `argv` can be -h or --help, which returns :help. 15 | 16 | Otherwise it is a github user name, project name, and (optionally) the number 17 | of entries to format. 18 | 19 | Return a tuple of `{ user, project, count }`, or `:help` if help was given. 20 | """ 21 | def parse_args(argv) do 22 | parse = OptionParser.parse(argv, switches: [ help: :boolean], aliases: [ h: :help]) 23 | 24 | case parse do 25 | { [ help: true ], _, _ } -> :help 26 | 27 | { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) } 28 | 29 | { _, [ user, project ], _ } -> { user, project, @default_count } 30 | 31 | _ -> :help 32 | end 33 | end 34 | 35 | def process({user, project, count}) do 36 | Issues.GithubIssues.fetch(user, project) 37 | |> decode_response 38 | |> convert_to_list_of_hashdicts 39 | |> sort_into_ascending_order 40 | |> Enum.take(count) 41 | end 42 | 43 | def decode_response({:ok, body}), do: body 44 | def decode_response({:error, error}) do 45 | {_, message} = List.keyfind(error, "message", 0) 46 | IO.puts "Error fetching from Github: #{message}" 47 | System.halt(2) 48 | end 49 | 50 | def convert_to_list_of_hashdicts(list) do 51 | list 52 | |> Enum.map(&Enum.into(&1, HashDict.new)) 53 | end 54 | 55 | def sort_into_ascending_order(list_of_issues) do 56 | Enum.sort list_of_issues, 57 | fn i1, i2 -> i1["created_at"] <= i2["created_at"] end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/lib/issues/github_issues.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues.GithubIssues do 2 | @user_agent [ {"User-agent", "Elixir pcewing00@gmail.com"} ] 3 | @github_url Application.get_env(:issues, :github_url) 4 | 5 | def fetch(user, project) do 6 | issues_url(user, project) 7 | |> HTTPoison.get(@user_agent) 8 | |> handle_response 9 | end 10 | def issues_url(user, project) do 11 | "#{@github_url}/repos/#{user}/#{project}/issues" 12 | end 13 | def handle_response({:ok, %{status_code: 200, body: body}}) do 14 | {:ok, :jsx.decode(body)} 15 | end 16 | def handle_response({:ok, %{status_code: _, body: body}}) do 17 | {:error, :jsx.decode(body)} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Issues.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :issues, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger, :httpoison]] 15 | end 16 | 17 | defp deps do 18 | [ 19 | {:httpoison, "~> 0.8.3"}, 20 | {:jsx, "~> 2.8"} 21 | ] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, 2 | "hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]}, 3 | "httpoison": {:hex, :httpoison, "0.8.3", "b675a3fdc839a0b8d7a285c6b3747d6d596ae70b6ccb762233a990d7289ccae4", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, 4 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 5 | "jsx": {:hex, :jsx, "2.8.0", "749bec6d205c694ae1786d62cea6cc45a390437e24835fd16d12d74f07097727", [:mix, :rebar], []}, 6 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 7 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 8 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}} 9 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/test/cli_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliTest do 2 | use ExUnit.Case 3 | 4 | import Issues.CLI, only: [ parse_args: 1, 5 | sort_into_ascending_order: 1, 6 | convert_to_list_of_hashdicts: 1] 7 | 8 | test ":help returned by option parsing with -h and --help options" do 9 | assert parse_args(["-h", "anything"]) == :help 10 | assert parse_args(["-help", "anything"]) == :help 11 | end 12 | 13 | test "three values returned if three given" do 14 | assert parse_args(["user", "project", "99"]) == { "user", "project", 99} 15 | end 16 | 17 | test "count is defaulted if two values given" do 18 | assert parse_args(["user", "project"]) == { "user", "project", 4} 19 | end 20 | 21 | test "sort ascending orders the correct way" do 22 | result = sort_into_ascending_order(fake_created_at_list(["c", "a", "b"])) 23 | issues = for issue <- result, do: issue["created_at"] 24 | assert issues == ~w{a b c} 25 | end 26 | 27 | defp fake_created_at_list(values) do 28 | data = for value <- values, 29 | do: [{"created_at", value}, {"other_data", "xxx"}] 30 | convert_to_list_of_hashdicts data 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/test/issues_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IssuesTest do 2 | use ExUnit.Case 3 | doctest Issues 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-3/issues/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | config :issues, github_url: "https://api.github.com" 3 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/lib/issues.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues do 2 | end 3 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/lib/issues/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues.CLI do 2 | @default_count 4 3 | 4 | @moduledoc """ 5 | Handle the command line parsing and the dispatch to the various functions that 6 | end up generating a table of the last _n_ issues in a github project. 7 | """ 8 | 9 | def run(argv) do 10 | parse_args(argv) 11 | end 12 | 13 | @doc """ 14 | `argv` can be -h or --help, which returns :help. 15 | 16 | Otherwise it is a github user name, project name, and (optionally) the number 17 | of entries to format. 18 | 19 | Return a tuple of `{ user, project, count }`, or `:help` if help was given. 20 | """ 21 | def parse_args(argv) do 22 | parse = OptionParser.parse(argv, switches: [ help: :boolean], aliases: [ h: :help]) 23 | 24 | case parse do 25 | { [ help: true ], _, _ } -> :help 26 | 27 | { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) } 28 | 29 | { _, [ user, project ], _ } -> { user, project, @default_count } 30 | 31 | _ -> :help 32 | end 33 | end 34 | 35 | def process({user, project, count}) do 36 | Issues.GithubIssues.fetch(user, project) 37 | |> decode_response 38 | |> convert_to_list_of_hashdicts 39 | |> sort_into_ascending_order 40 | |> Enum.take(count) 41 | end 42 | 43 | def decode_response({:ok, body}), do: body 44 | def decode_response({:error, error}) do 45 | {_, message} = List.keyfind(error, "message", 0) 46 | IO.puts "Error fetching from Github: #{message}" 47 | System.halt(2) 48 | end 49 | 50 | def convert_to_list_of_hashdicts(list) do 51 | list 52 | |> Enum.map(&Enum.into(&1, HashDict.new)) 53 | end 54 | 55 | def sort_into_ascending_order(list_of_issues) do 56 | Enum.sort list_of_issues, 57 | fn i1, i2 -> i1["created_at"] <= i2["created_at"] end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/lib/issues/formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues.Formatter do 2 | def format(issues) do 3 | issues 4 | |> filter 5 | |> print 6 | end 7 | 8 | def filter(issues) do 9 | Enum.map issues, 10 | fn issue -> 11 | created_at = HashDict.get(issue, "created_at") 12 | number = HashDict.get(issue, "number") 13 | title = HashDict.get(issue, "title") 14 | %{created_at: created_at, number: number, title: title} 15 | end 16 | end 17 | 18 | def print(issues) do 19 | max_num_length = get_max_num_length(issues) 20 | max_created_at_length = get_max_created_at_length(issues) 21 | max_title_length = get_max_title_length(issues) 22 | 23 | num_header = "#" 24 | length_diff = max_num_length - String.length(num_header) 25 | num_header = " #{num_header}#{String.duplicate(" ", length_diff)} " 26 | 27 | created_at_header = "created_at" 28 | length_diff = max_created_at_length - String.length(created_at_header) 29 | created_at_header = " #{created_at_header}#{String.duplicate(" ", length_diff)} " 30 | 31 | title_header = "title" 32 | length_diff = max_title_length - String.length(title_header) 33 | title_header = " #{title_header}#{String.duplicate(" ", length_diff)} " 34 | 35 | IO.puts "#{num_header}|#{created_at_header}|#{title_header}" 36 | IO.puts "#{String.duplicate("-", max_num_length + 2)}+#{String.duplicate("-", max_created_at_length + 2)}+#{String.duplicate("-", max_title_length + 2)}" 37 | 38 | Enum.each issues, 39 | fn issue -> 40 | num_string = Integer.to_string(issue.number) 41 | num_length_diff = max_num_length - String.length(num_string) 42 | number = " #{num_string}#{String.duplicate(" ", num_length_diff)} " 43 | 44 | created_at_length_diff = max_created_at_length - String.length(issue.created_at) 45 | created_at = " #{issue.created_at}#{String.duplicate(" ", created_at_length_diff)} " 46 | 47 | title_length_diff = max_title_length - String.length(issue.title) 48 | title = " #{issue.title}#{String.duplicate(" ", title_length_diff)} " 49 | 50 | IO.puts "#{number}|#{created_at}|#{title}" 51 | end 52 | end 53 | 54 | def get_max_num_length(issues) do 55 | issue = Enum.max_by issues, 56 | fn issue -> 57 | Integer.to_string(issue.number) 58 | |> String.length 59 | end 60 | Integer.to_string(issue.number) 61 | |> String.length 62 | end 63 | 64 | def get_max_created_at_length(issues) do 65 | issue = Enum.max_by issues, 66 | fn issue -> 67 | String.length(issue.created_at) 68 | end 69 | String.length(issue.created_at) 70 | end 71 | 72 | def get_max_title_length(issues) do 73 | issue = Enum.max_by issues, 74 | fn issue -> 75 | String.length(issue.title) 76 | end 77 | String.length(issue.title) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/lib/issues/github_issues.ex: -------------------------------------------------------------------------------- 1 | defmodule Issues.GithubIssues do 2 | @user_agent [ {"User-agent", "Elixir pcewing00@gmail.com"} ] 3 | @github_url Application.get_env(:issues, :github_url) 4 | 5 | def fetch(user, project) do 6 | issues_url(user, project) 7 | |> HTTPoison.get(@user_agent) 8 | |> handle_response 9 | end 10 | def issues_url(user, project) do 11 | "#{@github_url}/repos/#{user}/#{project}/issues" 12 | end 13 | def handle_response({:ok, %{status_code: 200, body: body}}) do 14 | {:ok, :jsx.decode(body)} 15 | end 16 | def handle_response({:ok, %{status_code: _, body: body}}) do 17 | {:error, :jsx.decode(body)} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Issues.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :issues, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger, :httpoison]] 15 | end 16 | 17 | defp deps do 18 | [ 19 | {:httpoison, "~> 0.8.3"}, 20 | {:jsx, "~> 2.8"} 21 | ] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, 2 | "hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]}, 3 | "httpoison": {:hex, :httpoison, "0.8.3", "b675a3fdc839a0b8d7a285c6b3747d6d596ae70b6ccb762233a990d7289ccae4", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, 4 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 5 | "jsx": {:hex, :jsx, "2.8.0", "749bec6d205c694ae1786d62cea6cc45a390437e24835fd16d12d74f07097727", [:mix, :rebar], []}, 6 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 7 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 8 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}} 9 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/test/cli_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CliTest do 2 | use ExUnit.Case 3 | 4 | import Issues.CLI, only: [ parse_args: 1, 5 | sort_into_ascending_order: 1, 6 | convert_to_list_of_hashdicts: 1] 7 | 8 | test ":help returned by option parsing with -h and --help options" do 9 | assert parse_args(["-h", "anything"]) == :help 10 | assert parse_args(["-help", "anything"]) == :help 11 | end 12 | 13 | test "three values returned if three given" do 14 | assert parse_args(["user", "project", "99"]) == { "user", "project", 99} 15 | end 16 | 17 | test "count is defaulted if two values given" do 18 | assert parse_args(["user", "project"]) == { "user", "project", 4} 19 | end 20 | 21 | test "sort ascending orders the correct way" do 22 | result = sort_into_ascending_order(fake_created_at_list(["c", "a", "b"])) 23 | issues = for issue <- result, do: issue["created_at"] 24 | assert issues == ~w{a b c} 25 | end 26 | 27 | defp fake_created_at_list(values) do 28 | data = for value <- values, 29 | do: [{"created_at", value}, {"other_data", "xxx"}] 30 | convert_to_list_of_hashdicts data 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/test/issues_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IssuesTest do 2 | use ExUnit.Case 3 | doctest Issues 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-4/issues/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-6/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OrganizingAProject-6 2 | In the United States, the National Oceanic and Atmospheric Administration provides hourly XML feeds of conditions at 1,800 locations. For example, the feed for a small airport close to where I'm writing this is at *http://w1.weather.gov/xml/current_obs/KDTO.xml*. 3 | 4 | Write an application that fetches this data, parses it, and displays it in a nice format. 5 | 6 | (Hint: You might not have to download a library to handle XML parsing.) 7 | 8 | ## Solution 9 | See the [weather](./weather) directory for the solution. Here it is run in *iex*: 10 | 11 | I could have used Erlang's *xmerl* library but chose to use *SweetXml* instead; it's a nice little wrapper for it. 12 | 13 | ``` 14 | iex> Weather.check 15 | Weather at Denton Municipal Airport, TX 16 | --------------------------------------- 17 | Time: 4:53 pm CDT 18 | Temperature: 92.0° F 19 | Winds: 12.7 MPH to the South 20 | ``` 21 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-6/weather/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-6/weather/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | config :weather, url: "http://w1.weather.gov/xml/current_obs/KDTO.xml" 3 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-6/weather/lib/weather.ex: -------------------------------------------------------------------------------- 1 | defmodule Weather do 2 | @url Application.get_env(:weather, :url) 3 | 4 | import SweetXml 5 | 6 | def check do 7 | HTTPoison.get(@url, []) 8 | |> handle_response 9 | |> print 10 | end 11 | 12 | def handle_response({:ok, %{status_code: 200, body: body}}), do: deserialize(body) 13 | def handle_response({:ok, %{status_code: code}}), do: {:error, {"Bad status code: #{code}"}} 14 | def handle_response({:error, error}), do: {:error, error} 15 | 16 | def deserialize(xml) do 17 | location = xml |> xpath(~x"//location/text()") |> List.to_string 18 | datetime = xml |> xpath(~x"//observation_time/text()") |> List.to_string 19 | [_date, time] = String.split(datetime, ", ") 20 | temp = xml |> xpath(~x"//temp_f/text()") |> List.to_string 21 | wind_dir = xml |> xpath(~x"//wind_dir/text()") |> List.to_string 22 | wind_mph = xml |> xpath(~x"//wind_mph/text()") |> List.to_string 23 | %{location: location, time: time, temp: temp, wind_dir: wind_dir, wind_mph: wind_mph} 24 | end 25 | 26 | def print(data) do 27 | header = "Weather at #{data.location}" 28 | IO.puts header 29 | IO.puts String.duplicate("-", String.length(header)) 30 | IO.puts "Time: #{data.time}" 31 | IO.puts "Temperature: #{data.temp}° F" 32 | IO.puts "Winds: #{data.wind_mph} MPH to the #{data.wind_dir}" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-6/weather/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Weather.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :weather, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger, :httpoison]] 15 | end 16 | 17 | defp deps do 18 | [ 19 | {:httpoison, "~> 0.8.3"}, 20 | {:sweet_xml, "~> 0.6.1"} 21 | ] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-6/weather/mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, 2 | "hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]}, 3 | "httpoison": {:hex, :httpoison, "0.8.3", "b675a3fdc839a0b8d7a285c6b3747d6d596ae70b6ccb762233a990d7289ccae4", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, 4 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 5 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 6 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 7 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}, 8 | "sweet_xml": {:hex, :sweet_xml, "0.6.1", "a56f235171f35a32807ce44798dedc748ce249fca574674fecd29c1321cab0de", [:mix], []}} 9 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-6/weather/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch13/OrganizingAProject-6/weather/test/weather_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WeatherTest do 2 | use ExUnit.Case 3 | doctest Weather 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: WorkingWithMultipleProcesses-1 2 | Run this code on your machine; see if you get comparable results. 3 | 4 | ## Solution 5 | See the [chain.exs](./chain.exs) file for the full module. 6 | 7 | I saw similar behavior up until the run with 1,000,000 processes. It may be related to running this within a VM but running the million processes actually froze the VM for some time before it finished. Oddly enough, the run with 400,000 processes was actually considerably faster on my machine than documented in the book. This aligns with the execution times of the previous runs as well so It shall remain a mystery why the million processes tanked my system. 8 | ``` 9 | $ elixir -r chain.exs -e "Chain.run(10)" 10 | {3154, "Result is 10"} 11 | $ elixir -r chain.exs -e "Chain.run(100)" 12 | {2259, "Result is 100"} 13 | $ elixir -r chain.exs -e "Chain.run(1000)" 14 | {2817, "Result is 1000"} 15 | $ elixir -r chain.exs -e "Chain.run(10000)" 16 | {9461, "Result is 10000"} 17 | $ elixir --erl "+P 1000000" -r chain.exs -e "Chain.run(400_000)" 18 | {1156269, "Result is 400000"} 19 | $ elixir --erl "+P 1000000" -r chain.exs -e "Chain.run(1_000_000)" 20 | {54507489, "Result is 1000000"} 21 | ``` 22 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-1/chain.exs: -------------------------------------------------------------------------------- 1 | defmodule Chain do 2 | def counter(next_pid) do 3 | receive do 4 | n -> send next_pid, n + 1 5 | end 6 | end 7 | 8 | def create_processes(n) do 9 | last = Enum.reduce 1..n, self, 10 | fn (_, send_to) -> 11 | spawn(Chain, :counter, [send_to]) 12 | end 13 | 14 | send last, 0 15 | 16 | receive do 17 | final_answer when is_integer(final_answer) -> 18 | "Result is #{inspect(final_answer)}" 19 | end 20 | end 21 | 22 | def run(n) do 23 | IO.puts inspect :timer.tc(Chain, :create_processes, [n]) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-2/README.md: -------------------------------------------------------------------------------- 1 | #Exercise: WorkingWithMultipleProcesses-2 2 | Write a program that spawns two processes and then passes each a unique token (for example, "fred" and "betty"). Have them send the tokens back. 3 | - Is the order in which the replies are received deterministic in theory? In practice? 4 | - If either answer is no, how could you make it so? 5 | 6 | ## Solution 7 | See the [processes.exs](./processes.exs) file for the full module. 8 | 9 | In theory, the order should not be deterministic. Whichever process finishes first may respond first. In practice, it appears that the first process to be sent a message is also the first process to respond. Several executions indicated this. 10 | 11 | If we wanted to be sure that one responded before the other, we could send a message to both child processes, but have the second child process also expect a message from the first. The first child process could respond to the parent as well as send a message to the second, and when the second process has received a message from the parent and the first child, it could then respond. 12 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-2/processes.exs: -------------------------------------------------------------------------------- 1 | defmodule Processes do 2 | def run do 3 | betty = spawn(Processes, :betty, []) 4 | fred = spawn(Processes, :fred, []) 5 | 6 | send betty, {self, "betty"} 7 | send fred, {self, "fred"} 8 | 9 | receive do 10 | {_sender, "betty"} -> IO.puts "Betty replied first!" 11 | {_sender, "fred"} -> IO.puts "Fred replied first!" 12 | end 13 | 14 | receive do 15 | {_sender, "betty"} -> IO.puts "Betty replied second!" 16 | {_sender, "fred"} -> IO.puts "Fred replied second!" 17 | end 18 | end 19 | 20 | def betty do 21 | receive do 22 | {sender, "betty"} -> send sender, {self, "betty"} 23 | end 24 | end 25 | 26 | def fred do 27 | receive do 28 | {sender, "fred"} -> send sender, {self, "fred"} 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-3/README.md: -------------------------------------------------------------------------------- 1 | #Exercise: WorkingWithMultipleProcesses-3 2 | Use *spawn_link* to start a process, and have that process send a message to the parent and then exit immediately. Meanwhile, sleep for 500 ms in the parent, then receive as many messages as are waiting. Trace what you receive. Does it matter that you weren't waiting for the notification from the child when it exited? 3 | 4 | ## Solution 5 | See the [processes.exs](./processes.exs) file for the full module. 6 | 7 | It seems that it does matter that we weren't waiting for the message when it was sent. When executing the code: 8 | ``` 9 | iex> Processes.run 10 | Message: Hello! 11 | All messages received. 12 | :ok 13 | ``` 14 | 15 | We only receive one message; on the second *receive* we timeout while waiting. 16 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-3/processes.exs: -------------------------------------------------------------------------------- 1 | defmodule Processes do 2 | def run do 3 | spawn_link(Processes, :child, [self]) 4 | 5 | :timer.sleep(500) 6 | 7 | loop_receive 8 | end 9 | 10 | def loop_receive do 11 | receive do 12 | message -> 13 | IO.puts "Message: #{message}" 14 | loop_receive 15 | after 500 -> IO.puts "All messages received." 16 | end 17 | end 18 | 19 | def child(parent) do 20 | send parent, "Hello!" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-4/README.md: -------------------------------------------------------------------------------- 1 | #Exercise: WorkingWithMultipleProcesses-4 2 | Do the same, but have the child raise an exception. What difference do you see in the tracing? 3 | 4 | ## Solution 5 | See the [processes.exs](./processes.exs) file for the full module. 6 | 7 | This time it looks like the child dies and takes the parent with it. Because the child died before the parent woke up and received the message it sent, the parent never receives the message because it dies while sleeping. 8 | ``` 9 | iex> Processes.run 10 | ** (EXIT from #PID<0.86.0>) an exception was raised: 11 | ** (RuntimeError) runtime error 12 | processes.exs:21: Processes.child/1 13 | 14 | Interactive Elixir (1.2.6) - press Ctrl+C to exit (type h() ENTER for help) 15 | iex> 16 | 12:53:30.686 [error] Process #PID<0.90.0> raised an exception 17 | ** (RuntimeError) runtime error 18 | processes.exs:21: Processes.child/1 19 | ``` 20 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-4/processes.exs: -------------------------------------------------------------------------------- 1 | defmodule Processes do 2 | def run do 3 | spawn_link(Processes, :child, [self]) 4 | 5 | :timer.sleep(500) 6 | 7 | loop_receive 8 | end 9 | 10 | def loop_receive do 11 | receive do 12 | message -> 13 | IO.puts "Message: #{message}" 14 | loop_receive 15 | after 500 -> IO.puts "All messages received." 16 | end 17 | end 18 | 19 | def child(parent) do 20 | send parent, "Hello!" 21 | raise RuntimeError 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-5/README.md: -------------------------------------------------------------------------------- 1 | #Exercise: WorkingWithMultipleProcesses-5 2 | Repeat the two, changing *spawn_link* to *spawn_monitor*. 3 | 4 | ## Solution 5 | See the [processes_no_ex.exs](./processes_no_ex.exs) and [processes_ex.exs](./processes_ex.exs) files for the full modules. 6 | 7 | When running the implementation where the child does not raise an exception, we now see the *DOWN* message. 8 | ``` 9 | iex> ProcessesNoEx.run 10 | Message: "Hello!" 11 | Message: {:DOWN, #Reference<0.0.1.133>, :process, #PID<0.88.0>, :normal} 12 | All messages received. 13 | :ok 14 | ``` 15 | 16 | When running the version where the child does throw, we see the child die; however, the parent stays alive and wakes up to receive the message the child sent as well as the *DOWN* message containing the error. 17 | ``` 18 | iex> ProcessesEx.run 19 | 20 | 13:05:13.090 [error] Process #PID<0.88.0> raised an exception 21 | ** (RuntimeError) runtime error 22 | processes_ex.exs:21: ProcessesEx.child/1 23 | Message: "Hello!" 24 | Message: {:DOWN, #Reference<0.0.1.134>, :process, #PID<0.88.0>, {%RuntimeError{message: "runtime error"}, [{ProcessesEx, :child, 1, [file: 'processes_ex.exs', line: 21]}]}} 25 | All messages received. 26 | :ok 27 | ``` 28 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-5/processes_ex.exs: -------------------------------------------------------------------------------- 1 | defmodule ProcessesEx do 2 | def run do 3 | spawn_monitor(ProcessesEx, :child, [self]) 4 | 5 | :timer.sleep(500) 6 | 7 | loop_receive 8 | end 9 | 10 | def loop_receive do 11 | receive do 12 | message -> 13 | IO.puts "Message: #{inspect message}" 14 | loop_receive 15 | after 500 -> IO.puts "All messages received." 16 | end 17 | end 18 | 19 | def child(parent) do 20 | send parent, "Hello!" 21 | raise RuntimeError 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-5/processes_no_ex.exs: -------------------------------------------------------------------------------- 1 | defmodule ProcessesNoEx do 2 | def run do 3 | spawn_monitor(ProcessesNoEx, :child, [self]) 4 | 5 | :timer.sleep(500) 6 | 7 | loop_receive 8 | end 9 | 10 | def loop_receive do 11 | receive do 12 | message -> 13 | IO.puts "Message: #{inspect message}" 14 | loop_receive 15 | after 500 -> IO.puts "All messages received." 16 | end 17 | end 18 | 19 | def child(parent) do 20 | send parent, "Hello!" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-6/README.md: -------------------------------------------------------------------------------- 1 | #Exercise: WorkingWithMultipleProcesses-6 2 | In the *pmap* code, I assigned the value of *self* to the variable *me* at the top of the method and then used *me* as the target of the message returned by the spawned process. Why use a separate variable here? 3 | 4 | ## Solution 5 | See the [parallel.exs](./parallel.exs) file for the full module. 6 | 7 | Take a look at *line 6* and notice that both the variable *me* and the variable *self* are used. Why might this be? 8 | 9 | When that function is run, it is executed on the process that is spawned and thus, when we access *self*, it is going to return the *pid* of the child process, not the parent process. So, in order to use the parent's *pid* within the context of the child thread, we need to capture it in a separate variable. 10 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-6/parallel.exs: -------------------------------------------------------------------------------- 1 | defmodule Parallel do 2 | def pmap(collection, fun) do 3 | me = self 4 | collection 5 | |> Enum.map(fn (elem) -> 6 | spawn_link fn -> (send me, { self, fun.(elem) }) end 7 | end) 8 | |> Enum.map(fn (pid) -> 9 | receive do { ^pid, result } -> result end 10 | end) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-7/README.md: -------------------------------------------------------------------------------- 1 | #Exercise: WorkingWithMultipleProcesses-7 2 | Change the *^pid* in *pmap* to *_pid*. This means the receive block will take responses in the order the processes send them. Now run the code again. Do you see any difference in the output? If you're like me, you don't, but the program clearly contains a bug. Are you scared by this? Can you find a way to reveal the problem (perhaps by passing in a different function, by sleeping, or by increasing the number of processes)? Change it back to *^pid* and make sure the order is now correct. 3 | 4 | ## Solution 5 | See the [parallel.exs](./parallel.exs) file for the full module. 6 | 7 | After making the change, we can see that the processes still execute in the order we want: 8 | ``` 9 | iex> Parallel.pmap [1, 2, 3], &(&1 + 1) 10 | [2, 3, 4] 11 | ``` 12 | 13 | However, this isn't deterministic. What if the function we pass in to the *pmap* has a less consistent execution time? Let's try passing in the function below, where each execution of the function will sleep for a random amount of time between 0 and 1999 milliseconds. This is similar to how IO operations might perform. Let's try it: 14 | ``` 15 | iex> sleep_add = fn num -> 16 | ...> milliseconds = :rand.uniform(2000) 17 | ...> :timer.sleep(milliseconds) 18 | ...> num + 1 19 | ...> end 20 | #Function<6.52032458/1 in :erl_eval.expr/5> 21 | iex> Parallel.pmap [1, 2, 3], &(sleep_add.(&1)) 22 | [3, 2, 4] 23 | ``` 24 | 25 | This time the results were obviously out of order. Let's change the *_pid* back to *^pid* but still use the *sleep_add* function and verify that the results are processed in order again: 26 | ``` 27 | iex> sleep_add = fn num -> 28 | ...> milliseconds = :rand.uniform(2000) 29 | ...> :timer.sleep(milliseconds) 30 | ...> num + 1 31 | ...> end 32 | #Function<6.52032458/1 in :erl_eval.expr/5> 33 | iex> Parallel.pmap [1, 2, 3], &(sleep_add.(&1)) 34 | [2, 3, 4] 35 | iex> Parallel.pmap [1, 2, 3], &(sleep_add.(&1)) 36 | [2, 3, 4] 37 | iex> Parallel.pmap [1, 2, 3], &(sleep_add.(&1)) 38 | [2, 3, 4] 39 | ``` 40 | I ran the *pmap* a few times just to be sure we weren't getting lucky with the random milliseconds, but it looks like the results are now processed in order again! 41 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-7/parallel.exs: -------------------------------------------------------------------------------- 1 | defmodule Parallel do 2 | def pmap(collection, fun) do 3 | me = self 4 | collection 5 | |> Enum.map(fn (elem) -> 6 | spawn_link fn -> (send me, { self, fun.(elem) }) end 7 | end) 8 | |> Enum.map(fn (pid) -> 9 | receive do { ^pid, result } -> result end 10 | end) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-8/README.md: -------------------------------------------------------------------------------- 1 | #Exercise: WorkingWithMultipleProcesses-8 2 | Run the Fibonacci code on your machine. Do you get comparable timings? If your machine has multiple cores and/or processors, do you see improvements in the timing as we increase the application's concurrency? 3 | 4 | ## Solution 5 | See the [fibonacci.exs](./fibonacci.exs) file for the full modules. 6 | 7 | So, I see no performance increase whatsoever: 8 | ``` 9 | iex> Runner.run 10 | [{37, 24157817}, {37, 24157817}, {37, 24157817}, {37, 24157817}, {37, 24157817}, {37, 24157817}] 11 | 12 | # time (s) 13 | 1 5.90 14 | 2 5.85 15 | 3 5.86 16 | 4 5.91 17 | 5 5.97 18 | 6 5.87 19 | 7 5.86 20 | 8 5.90 21 | 9 5.86 22 | 10 5.84 23 | :ok 24 | ``` 25 | 26 | I'm on a multi-core machine; however, I'm running this in a VM that is only allocated 1 processor. Maybe when I'm at home I'll try running this again. I would expect the results to be more in line with those in the book. 27 | 28 | I'm a little confused why the results in the book show: `{37, 39088169}`; *fib(37)* should equal 24,157,817. Oh well. 29 | 30 | ### At Home 31 | So, the previous run was on my work computer but I tried the same thing again at home and saw interesting results: 32 | ``` 33 | iex> Runner.run 34 | [{37, 24157817}, {37, 24157817}, {37, 24157817}, {37, 24157817}, {37, 24157817}, {37, 24157817}] 35 | 36 | # time (s) 37 | 1 7.90 38 | 2 4.11 39 | 3 2.76 40 | 4 2.72 41 | 5 2.90 42 | 6 1.93 43 | 7 1.91 44 | 8 1.95 45 | 9 1.93 46 | 10 1.91 47 | :ok 48 | ``` 49 | 50 | The single processor run was actually slower; however, adding processes significantly increased the performance. My home PC is running an i7 2600k, which has 4 cores and 8 threads, so it makes sense that there were performance gains. I'm unsure of why it levels off between 3 and 6 and then at 7 instead of 8 but regardless, it's pretty cool to see the power of concurrency. From 7.9 down to ~1.9 seconds is a pretty awesome increase. 51 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-8/fibonacci.exs: -------------------------------------------------------------------------------- 1 | defmodule FibSolver do 2 | def fib(scheduler) do 3 | send scheduler, {:ready, self} 4 | receive do 5 | {:fib, n, client} -> 6 | send client, {:answer, n, fib_calc(n), self} 7 | fib(scheduler) 8 | {:shutdown} -> 9 | exit(:normal) 10 | end 11 | end 12 | 13 | defp fib_calc(0), do: 0 14 | defp fib_calc(1), do: 1 15 | defp fib_calc(n), do: fib_calc(n-1) + fib_calc(n-2) 16 | end 17 | 18 | defmodule Scheduler do 19 | def run(num_processes, module, func, to_calculate) do 20 | (1..num_processes) 21 | |> Enum.map(fn(_) -> spawn(module, func, [self]) end) 22 | |> schedule_processes(to_calculate, []) 23 | end 24 | 25 | defp schedule_processes(processes, queue, results) do 26 | receive do 27 | {:ready, pid} when length(queue) > 0 -> 28 | [next | tail] = queue 29 | send pid, {:fib, next, self} 30 | schedule_processes(processes, tail, results) 31 | 32 | {:ready, pid} -> 33 | send pid, {:shutdown} 34 | if length(processes) > 1 do 35 | schedule_processes(List.delete(processes, pid), queue, results) 36 | else 37 | Enum.sort(results, fn {n1, _}, {n2, _} -> n1 <= n2 end) 38 | end 39 | 40 | {:answer, number, result, _pid} -> 41 | schedule_processes(processes, queue, [{number, result} | results]) 42 | end 43 | end 44 | end 45 | 46 | defmodule Runner do 47 | def run do 48 | to_process = [37, 37, 37, 37, 37, 37] 49 | 50 | Enum.each 1..10, fn num_processes -> 51 | {time, result} = :timer.tc(Scheduler, :run, [num_processes, FibSolver, :fib, to_process]) 52 | 53 | if num_processes == 1 do 54 | IO.puts inspect result 55 | IO.puts "\n # time (s)" 56 | end 57 | :io.format "~2B ~.2f~n", [num_processes, time/1000000.0] 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-9/.gitignore: -------------------------------------------------------------------------------- 1 | test_files/ 2 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-9/README.md: -------------------------------------------------------------------------------- 1 | #Exercise: WorkingWithMultipleProcesses-9 2 | Take this scheduler code and update it to let you run a function that finds the number of times the word "cat" appears in each file in a given directory. Run one server process per file. The function *File.ls!* returns the names of files in a directory, and *File.read!* reads the contents of a file as a binary. Can you write it as a more generalized scheduler? 3 | 4 | Run your code on a directory with a reasonable number of files (maybe around 100) so you can experiment with the effects of concurrency. 5 | 6 | ## Solution 7 | See the [word_count.exs](./word_count.exs) file for the full modules. 8 | 9 | For the purposes of testing I generated 100 files with somewhat randomized content. Each file contains 15000 sentences and there is a 25% chance that each sentence contains the word "cat". These files can be generated using the [generate_test_files.py](./generate_test_files.py) script. 10 | 11 | Here is the code being run in *iex*: 12 | ``` 13 | iex> Runner.run 14 | 15 | Inspecting the results: 16 | %{"test_files/file028.txt" => 3796, "test_files/file091.txt" => 3792, "test_files/file052.txt" => 3784, "test_files/file075.txt" => 3685, "test_files/file021.txt" => 3760, "test_files/file002.txt" => 3844, "test_files/file093.txt" => 3735, "test_files/file047.txt" => 3732, "test_files/file066.txt" => 3778, "test_files/file058.txt" => 3810, "test_files/file055.txt" => 3823, "test_files/file042.txt" => 3783, "test_files/file083.txt" => 3893, "test_files/file096.txt" => 3732, "test_files/file003.txt" => 3673, "test_files/file067.txt" => 3740, "test_files/file078.txt" => 3826, "test_files/file015.txt" => 3730, "test_files/file097.txt" => 3667, "test_files/file017.txt" => 3668, "test_files/file068.txt" => 3750, "test_files/file088.txt" => 3814, "test_files/file053.txt" => 3781, "test_files/file071.txt" => 3699, "test_files/file098.txt" => 3770, "test_files/file023.txt" => 3765, "test_files/file084.txt" => 3802, "test_files/file010.txt" => 3743, "test_files/file035.txt" => 3676, "test_files/file063.txt" => 3762, "test_files/file050.txt" => 3829, "test_files/file007.txt" => 3696, "test_files/file027.txt" => 3728, "test_files/file072.txt" => 3762, "test_files/file090.txt" => 3854, "test_files/file025.txt" => 3745, "test_files/file056.txt" => 3767, "test_files/file012.txt" => 3787, "test_files/file033.txt" => 3744, "test_files/file001.txt" => 3769, "test_files/file024.txt" => 3786, "test_files/file060.txt" => 3711, "test_files/file032.txt" => 3709, "test_files/file059.txt" => 3735, "test_files/file094.txt" => 3659, "test_files/file077.txt" => 3745, "test_files/file089.txt" => 3675, "test_files/file082.txt" => 3688, "test_files/file040.txt" => 3815, "test_files/file030.txt" => 3799, ...} 17 | 18 | 19 | 20 | # time (ms) 21 | 1 239.18 22 | 2 237.31 23 | 3 282.14 24 | 4 239.86 25 | 5 283.02 26 | 6 255.72 27 | 7 243.07 28 | 8 244.58 29 | 9 245.02 30 | 10 283.94 31 | ``` 32 | 33 | I'm not surprised that a single process performs pretty much just as well or better than multiple processes given that the VM this is running is only has a single core. I'd like to run this again on my home PC to see the difference. 34 | 35 | ### At Home 36 | Running this again on my home PC I see the following performance: 37 | ``` 38 | iex> Runner.run 39 | 40 | Inspecting the results: 41 | %{"test_files/file028.txt" => 3761, "test_files/file091.txt" => 3840, "test_files/file052.txt" => 3696, "test_files/file075.txt" => 3819, "test_files/file021.txt" => 3761, "test_files/file002.txt" => 3755, "test_files/file093.txt" => 3720, "test_files/file047.txt" => 3766, "test_files/file066.txt" => 3832, "test_files/file058.txt" => 3744, "test_files/file055.txt" => 3727, "test_files/file042.txt" => 3784, "test_files/file083.txt" => 3646, "test_files/file096.txt" => 3761, "test_files/file003.txt" => 3787, "test_files/file067.txt" => 3660, "test_files/file078.txt" => 3707, "test_files/file015.txt" => 3713, "test_files/file097.txt" => 3752, "test_files/file017.txt" => 3682, "test_files/file068.txt" => 3713, "test_files/file088.txt" => 3733, "test_files/file053.txt" => 3818, "test_files/file071.txt" => 3827, "test_files/file098.txt" => 3674, "test_files/file023.txt" => 3739, "test_files/file084.txt" => 3739, "test_files/file010.txt" => 3850, "test_files/file035.txt" => 3778, "test_files/file063.txt" => 3807, "test_files/file050.txt" => 3719, "test_files/file007.txt" => 3783, "test_files/file027.txt" => 3765, "test_files/file072.txt" => 3785, "test_files/file090.txt" => 3721, "test_files/file025.txt" => 3801, "test_files/file056.txt" => 3744, "test_files/file012.txt" => 3707, "test_files/file033.txt" => 3771, "test_files/file001.txt" => 3779, "test_files/file024.txt" => 3805, "test_files/file060.txt" => 3773, "test_files/file032.txt" => 3728, "test_files/file059.txt" => 3696, "test_files/file094.txt" => 3762, "test_files/file077.txt" => 3857, "test_files/file089.txt" => 3805, "test_files/file082.txt" => 3689, "test_files/file040.txt" => 3720, "test_files/file030.txt" => 3748, ...} 42 | 43 | 44 | 45 | # time (ms) 46 | 1 207.98 47 | 2 96.31 48 | 3 78.04 49 | 4 67.39 50 | 5 60.59 51 | 6 49.32 52 | 7 58.59 53 | 8 49.99 54 | 9 60.67 55 | 10 46.11 56 | :ok 57 | ``` 58 | 59 | Nice! From a 208ms execution time down to a 49ms execution time going from 1 to 6 processes. There seems to be some fluctuation and I would think this has something to do with the inconsistency of disk read/write but regardless the execution time was cut down to a quarter of the original by handling the work concurrently. 60 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-9/generate_test_files.py: -------------------------------------------------------------------------------- 1 | import os, random 2 | 3 | # Global variables. 4 | sentences = [ 5 | 'The cow jumped over the moon.', 6 | 'The cat in the hat laid on the mat.', 7 | 'This little piggy went to the market.', 8 | 'It\'s a piece of cake to bake a pretty cake.' 9 | ] 10 | file_count = 100; 11 | sentence_count = 15000; 12 | file_name_prefix = 'file'; 13 | file_name_suffix = '.txt'; 14 | output_directory = 'test_files'; 15 | 16 | def main(): 17 | os.makedirs(output_directory) 18 | 19 | for file_index in range(0, file_count): 20 | file_name = '{0}{1:03d}{2}'.format(file_name_prefix, file_index, file_name_suffix) 21 | file_path = os.path.join(output_directory, file_name) 22 | 23 | random.seed() 24 | with open(file_path, 'w+') as test_file: 25 | for x in range(0, sentence_count): 26 | sentence_index = random.randint(0, 3) 27 | test_file.write('{0}{1}'.format(sentences[sentence_index], '\n')) 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /ch14/WorkingWithMultipleProcesses-9/word_count.exs: -------------------------------------------------------------------------------- 1 | defmodule WordCounter do 2 | def count(scheduler) do 3 | send scheduler, {:ready, self} 4 | receive do 5 | {:count, file, word, client} -> 6 | send client, {:answer, file, word, count_word(file, word), self} 7 | count(scheduler) 8 | {:shutdown} -> 9 | exit(:normal) 10 | end 11 | end 12 | 13 | def count_word(file, word) do 14 | content = File.read!(file) 15 | tokens = String.split(content, word) 16 | length(tokens) - 1 17 | end 18 | end 19 | 20 | defmodule Scheduler do 21 | def run(num_processes, module, func, directory, word) do 22 | {:ok, files} = File.ls(directory) 23 | files = Enum.map(files, fn file -> Path.join(directory, file) end) 24 | 25 | (1..num_processes) 26 | |> Enum.map(fn(_) -> spawn(module, func, [self]) end) 27 | |> schedule_processes(files, word, Map.new) 28 | end 29 | 30 | defp schedule_processes(processes, queue, word, results) do 31 | receive do 32 | {:ready, pid} when length(queue) > 0 -> 33 | [next | tail] = queue 34 | send pid, {:count, next, word, self} 35 | schedule_processes(processes, tail, word, results) 36 | 37 | {:ready, pid} -> 38 | send pid, {:shutdown} 39 | if length(processes) > 1 do 40 | schedule_processes(List.delete(processes, pid), queue, word, results) 41 | else 42 | results 43 | end 44 | 45 | {:answer, file, _word, result, _pid} -> 46 | schedule_processes(processes, queue, word, Map.put(results, file, result)) 47 | end 48 | end 49 | end 50 | 51 | defmodule Runner do 52 | def run do 53 | directory = "test_files/" 54 | word = "cat" 55 | 56 | Enum.each 1..10, fn num_processes -> 57 | {time, result} = :timer.tc(Scheduler, :run, [num_processes, WordCounter, :count, directory, word]) 58 | 59 | if num_processes == 1 do 60 | IO.puts "\nInspecting the results:\n#{inspect result}\n\n" 61 | IO.puts "\n # time (ms)" 62 | end 63 | :io.format "~2B ~.2f~n", [num_processes, time/1000.0] 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /ch15/Nodes-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise Nodes-1 2 | Set up two terminal windows, and go to a different directory in each. Then start up a named node in each. In one window, write a function that lists the contents of the current directory. 3 | 4 | ``` 5 | fun = fn -> IO.puts(Enum.join(File.ls!, ",")) end 6 | ``` 7 | 8 | Run it twice, once on each node. 9 | 10 | ## Solution 11 | This isn't really a solution, but here's what the two executions look like. This is all we do in the first terminal: 12 | ``` 13 | me@my-pc:~/dev/spokes/client$ iex --sname node_one 14 | iex(node_one@my-pc)> Node.self 15 | :"node_one@my-pc" 16 | ``` 17 | 18 | In the second, we connect to the first, define the function, and then call it both locally and on the remote node. 19 | ``` 20 | iex(node_two@my-pc)> Node.connect :"node_one@my-pc" 21 | true 22 | iex(node_two@my-pc)> Node.list 23 | [:"node_one@my-pc"] 24 | iex(node_two@my-pc)> fun = fn -> IO.puts(Enum.join(File.ls!, ",")) end 25 | #Function<20.52032458/0 in :erl_eval.expr/5> 26 | iex(node_two@my-pc)> fun.() 27 | .git,.editorconfig,.gitignore,index.js,node_modules,lib,README.md,LICENSE,gulpfile.js,test,package.json 28 | :ok 29 | iex(node_two@my-pc)> Node.spawn(:"node_one@my-pc", fun) 30 | .git,.editorconfig,.gitignore,config,.babelrc,README.md,LICENSE,tasks,gulpfile.js,resources,erl_crash.dump,app,package.json 31 | #PID<8904.93.0> 32 | iex(node_two@my-pc)> 33 | ``` 34 | -------------------------------------------------------------------------------- /ch15/Nodes-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise Nodes-2 2 | When I introduced the internal server, I said it sent a tick "about every 2 seconds." But in the receive loop, it has an explicit timeout of 2,000 ms. Why did I say "about" when it looks as if the time should be pretty accurate? 3 | 4 | ## Solution 5 | The ticker should send a message pretty accurately every 2000ms; however, I would assume there is a certain overhead, albeit small, involved in the message being delivered to the remote node. This would be more apparent when the nodes are not both on the same machine. 6 | 7 | Also, every time a client registers, the 2000ms counter will restart. 8 | 9 | (There is also probably a couple microseconds or so of overhead just making the recursive call but I would think this is negligible.) 10 | -------------------------------------------------------------------------------- /ch15/Nodes-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise Nodes-3 2 | Alter the code so that successive ticks are sent to each registered client (so the first goes to the first client, the second to the next client, and so on). Once the last client receives a tick, the process starts back at the first. The solution should deal with new clients being added at any time. 3 | 4 | ## Solution 5 | See the [ticker.exs](./ticker.exs) file for the full module. 6 | 7 | To test this, I ran four terminals. The first was the server and the other three were the clients. The server waited for the client nodes to each connect and then listed the nodes it was aware of. Once all of the nodes were connected, the server started ticking. 8 | ``` 9 | $ iex --sname server ticker.exs 10 | iex(server@my-pc)> Node.list 11 | [:"client_c@my-pc", :"client_b@my-pc", :"client_a@my-pc"] 12 | iex(server@my-pc)> Ticker.Server.start 13 | :yes 14 | tick 15 | tick 16 | registering #PID<12403.116.0> 17 | tick 18 | tick 19 | registering #PID<12402.116.0> 20 | tick 21 | tick 22 | tick 23 | registering #PID<12401.116.0> 24 | tick 25 | tick 26 | tick 27 | tick 28 | tick 29 | tick 30 | tick 31 | tick 32 | tick 33 | ``` 34 | 35 | When the first client registers, it is the only one receiving messages. 36 | ``` 37 | $ iex --sname client_a ticker.exs 38 | iex(client_a@my-pc)> Node.connect(:"server@my-pc") 39 | true 40 | iex(client_a@my-pc)> Ticker.Client.start 41 | {:register, #PID<0.116.0>} 42 | tock in client 43 | tock in client 44 | tock in client 45 | tock in client 46 | tock in client 47 | tock in client 48 | ``` 49 | 50 | Once the second client registers, the ticks received are alternated between client a and b. 51 | ``` 52 | $ iex --sname client_b ticker.exs 53 | iex(client_b@my-pc)> Node.connect(:"server@my-pc") 54 | true 55 | iex(client_b@my-pc)> Ticker.Client.start 56 | {:register, #PID<0.116.0>} 57 | tock in client 58 | tock in client 59 | tock in client 60 | tock in client 61 | tock in client 62 | ``` 63 | 64 | Finally, once the third client registers, the ticks cycle through the three clients. 65 | ``` 66 | $ iex --sname client_c ticker.exs 67 | iex(client_c@my-pc)> Node.connect(:"server@my-pc") 68 | true 69 | iex(client_c@my-pc)> Ticker.Client.start 70 | {:register, #PID<0.116.0>} 71 | tock in client 72 | tock in client 73 | tock in client 74 | ``` 75 | 76 | The one interesting thing I hadn't thought about is this implementation actually cycles through the clients in the reverse order. This is because we are actually adding each new client to the front of the list but we iterate from front to back. We could easily fix this by changing the *current* logic to decrement instead of increment but I decided to just leave it because it doesn't really matter. 77 | -------------------------------------------------------------------------------- /ch15/Nodes-3/ticker.exs: -------------------------------------------------------------------------------- 1 | defmodule Ticker do 2 | defmodule Server do 3 | 4 | @interval 2000 5 | @name :ticker 6 | 7 | def start do 8 | pid = spawn(__MODULE__, :generator, [[], nil]) 9 | :global.register_name(@name, pid) 10 | end 11 | 12 | def register(client_pid) do 13 | send :global.whereis_name(@name), { :register, client_pid } 14 | end 15 | 16 | def generator(clients, current) do 17 | receive do 18 | { :register, pid } -> 19 | IO.puts "registering #{inspect pid}" 20 | if current == nil, do: current = 0 21 | generator([pid | clients], current) 22 | after 23 | @interval -> 24 | IO.puts "tick" 25 | 26 | unless current == nil do 27 | client = Enum.at(clients, current) 28 | send client, { :tick } 29 | 30 | current = current + 1 31 | if current >= length(clients), do: current = 0 32 | end 33 | 34 | generator(clients, current) 35 | end 36 | end 37 | end 38 | 39 | defmodule Client do 40 | 41 | def start do 42 | pid = spawn(__MODULE__, :receiver, []) 43 | Ticker.Server.register(pid) 44 | end 45 | 46 | def receiver do 47 | receive do 48 | { :tick } -> 49 | IO.puts "tock in client" 50 | receiver 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /ch15/Nodes-4/README.md: -------------------------------------------------------------------------------- 1 | # Exercise Nodes-4 2 | The ticker process in this chapter is a central server that sends events to registered clients. Reimplement this as a ring of clients. A client sends a tick to the next client in the ring. After 2 seconds, *that* client sends a tick to its next client. 3 | 4 | When thinking about how to add clients to the ring, remember to deal with the case where a client's receive loop times out just as you're adding a new process. What does this say about who has to be responsible for updating the links? 5 | 6 | ## Solution 7 | See the [ticker.exs](./ticker.exs) file for the full module. 8 | 9 | I'm implementing this using the idea of leaders and followers. The first client to register declares itself the leader. The leader is responsible for listening for registration requests from other clients as well as emitting the tick message to the next client in the chain. When the leader is the only one in the chain, it simply prints tick and continues being the leader. 10 | 11 | When the leader is not the only one in the chain, it will increment the index of the next leader, unregister its name globally, and then send the tick message along with the state to the next leader. 12 | 13 | The follower who receives the tick message will declare itself the leader by registering the leader name. It then takes on the leadership responsibilities previously mentioned. 14 | 15 | This is the initial client. You can see that until another client registers it simply ticks. 16 | ``` 17 | $ iex --sname a ticker.exs 18 | iex(a@my-pc)> Ticker.register 19 | tick 20 | tick 21 | tick 22 | registering #PID<12364.92.0> 23 | tick 24 | tock in client 25 | tick 26 | tock in client 27 | tick 28 | tock in client 29 | tick 30 | tock in client 31 | tick 32 | tock in client 33 | tick 34 | ``` 35 | 36 | When the second client registers, the two begin to tick tock, passing leadership back and forth. 37 | ``` 38 | $ iex --sname b ticker.exs 39 | iex(b@my-pc)> Node.connect(:"a@my-pc") 40 | true 41 | iex(b@my-pc)> Ticker.register 42 | tock in client 43 | tick 44 | tock in client 45 | tick 46 | tock in client 47 | registering #PID<12409.92.0> 48 | tick 49 | tock in client 50 | tick 51 | tock in client 52 | tick 53 | tock in client 54 | tick 55 | tock in client 56 | ``` 57 | 58 | When the third client registers, it becomes a circle with the leadership being passed around. 59 | ``` 60 | $ iex --sname c ticker.exs 61 | iex(c@my-pc)> Node.connect(:"a@my-pc") 62 | true 63 | iex(c@my-pc)> Ticker.register 64 | tock in client 65 | tick 66 | tock in client 67 | tick 68 | tock in client 69 | tick 70 | ``` 71 | 72 | There is definitely a race condition in this implementation, When the current leader unregisters its name, the new leader won't register the name until it has received the message. A new client could register in this time and take the leadership, effectively breaking the existing chain because the process won't be able to register the name. 73 | 74 | I'm not sure if there is a good way around this without adding some sort of centralization mechanism. 75 | -------------------------------------------------------------------------------- /ch15/Nodes-4/ticker.exs: -------------------------------------------------------------------------------- 1 | defmodule Ticker do 2 | 3 | @interval 2000 4 | @leader :leader 5 | 6 | def register do 7 | case :global.whereis_name(@leader) do 8 | pid when is_pid(pid) -> 9 | send pid, { :register, self } 10 | follow 11 | :undefined -> 12 | :global.register_name(@leader, self) 13 | lead([self], nil) 14 | end 15 | end 16 | 17 | def lead(clients, next) do 18 | receive do 19 | { :register, pid } -> 20 | IO.puts "registering #{inspect pid}" 21 | if next == nil, do: next = 0 22 | lead([pid | clients], next) 23 | after 24 | @interval -> 25 | IO.puts "tick" 26 | 27 | unless next == nil do 28 | next_client = Enum.at(clients, next) 29 | 30 | next = next - 1 31 | if next < 0 , do: next = length(clients) - 1 32 | 33 | :global.unregister_name(@leader) 34 | send next_client, { :tick, clients, next } 35 | follow 36 | else 37 | lead(clients, next) 38 | end 39 | end 40 | end 41 | 42 | def follow do 43 | receive do 44 | { :tick, clients, next } -> 45 | IO.puts "tock in client" 46 | :global.register_name(@leader, self) 47 | lead(clients, next) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OTP-Servers-1 2 | You're going to start creating a server that implements a stack. The call that initializes your stack will pass in a list of the initial stack contents. 3 | 4 | For now, implement only the *pop* interface. It's acceptable for your server to crash if someone tries to pop from an empty stack. 5 | 6 | For example, if initialized with *[5,"cat",9]*, successive calls to *pop* will return *5*, *"cat"*, and *9* 7 | 8 | ## Solution 9 | See the [stack](./stack) directory for the full project. 10 | 11 | ``` 12 | iex> {:ok, pid} = GenServer.start_link(Stack.Server, [5,"cat",9]) 13 | {:ok, #PID<0.125.0>} 14 | iex> GenServer.call(pid, :pop) 15 | 5 16 | iex> GenServer.call(pid, :pop) 17 | "cat" 18 | iex> GenServer.call(pid, :pop) 19 | 9 20 | ``` 21 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-1/stack/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-1/stack/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-1/stack/lib/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack do 2 | end 3 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-1/stack/lib/stack/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Server do 2 | use GenServer 3 | 4 | def init(state) do 5 | {:ok, state} 6 | end 7 | 8 | def handle_call(:pop, _from, state) do 9 | [item | tail] = state 10 | {:reply, item, tail} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-1/stack/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Stack.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :stack, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger]] 15 | end 16 | 17 | defp deps do 18 | [] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-1/stack/test/stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StackTest do 2 | use ExUnit.Case 3 | doctest Stack 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-1/stack/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OTP-Servers-2 2 | Extend your stack server with a *push* interface that adds a single value to the top of the stack. This will be implemented as a cast. 3 | 4 | Experiment in *iex* with pushing and popping values. 5 | 6 | ## Solution 7 | See the [stack](./stack) directory for the full project. 8 | 9 | ``` 10 | iex> {:ok, pid} = GenServer.start_link(Stack.Server, []) 11 | {:ok, #PID<0.112.0>} 12 | iex> GenServer.cast(pid, {:push, 1}) 13 | :ok 14 | iex> GenServer.cast(pid, {:push, 2}) 15 | :ok 16 | iex> GenServer.call(pid, :pop) 17 | 2 18 | iex> GenServer.call(pid, :pop) 19 | 1 20 | ``` 21 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-2/stack/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-2/stack/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-2/stack/lib/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack do 2 | end 3 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-2/stack/lib/stack/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Server do 2 | use GenServer 3 | 4 | def init(state) do 5 | {:ok, state} 6 | end 7 | 8 | def handle_cast({:push, item}, state) do 9 | {:noreply, [item | state]} 10 | end 11 | 12 | def handle_call(:pop, _from, state) do 13 | [item | tail] = state 14 | {:reply, item, tail} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-2/stack/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Stack.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :stack, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger]] 15 | end 16 | 17 | defp deps do 18 | [] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-2/stack/test/stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StackTest do 2 | use ExUnit.Case 3 | doctest Stack 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-2/stack/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OTP-Servers-3 2 | Give your stack server process a name, and make sure it is accessible by that name in *iex*. 3 | 4 | ## Solution 5 | See the [stack](./stack) directory for the full project. 6 | 7 | ``` 8 | iex> Stack.Server.start_link([1, 2, 3]) 9 | {:ok, #PID<0.112.0>} 10 | iex> GenServer.call(MyStack, :pop) 11 | 1 12 | ``` 13 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-3/stack/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-3/stack/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-3/stack/lib/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack do 2 | end 3 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-3/stack/lib/stack/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Server do 2 | use GenServer 3 | 4 | def start_link(state) do 5 | GenServer.start_link(__MODULE__, state, [name: MyStack]) 6 | end 7 | 8 | def handle_cast({:push, item}, state) do 9 | {:noreply, [item | state]} 10 | end 11 | 12 | def handle_call(:pop, _from, state) do 13 | [item | tail] = state 14 | {:reply, item, tail} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-3/stack/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Stack.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :stack, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger]] 15 | end 16 | 17 | defp deps do 18 | [] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-3/stack/test/stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StackTest do 2 | use ExUnit.Case 3 | doctest Stack 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-3/stack/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-4/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OTP-Servers-4 2 | Add the API to your stack module (the functions that wrap the GenServer calls). 3 | 4 | ## Solution 5 | See the [stack](./stack) directory for the full project. 6 | 7 | ``` 8 | iex> Stack.Server.start_link([1, 2, 3]) 9 | {:ok, #PID<0.125.0>} 10 | iex> Stack.Server.push("Hello") 11 | :ok 12 | iex> Stack.Server.pop 13 | "Hello" 14 | ``` 15 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-4/stack/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-4/stack/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-4/stack/lib/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack do 2 | end 3 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-4/stack/lib/stack/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Server do 2 | @name MyStack 3 | 4 | use GenServer 5 | 6 | def push(item) do 7 | GenServer.cast(@name, {:push, item}) 8 | end 9 | 10 | def pop do 11 | GenServer.call(@name, :pop) 12 | end 13 | 14 | ### 15 | # GenServer API 16 | ### 17 | 18 | def start_link(state) do 19 | GenServer.start_link(__MODULE__, state, [name: @name]) 20 | end 21 | 22 | def handle_cast({:push, item}, state) do 23 | {:noreply, [item | state]} 24 | end 25 | 26 | def handle_call(:pop, _from, state) do 27 | [item | tail] = state 28 | {:reply, item, tail} 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-4/stack/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Stack.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :stack, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger]] 15 | end 16 | 17 | defp deps do 18 | [] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-4/stack/test/stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StackTest do 2 | use ExUnit.Case 3 | doctest Stack 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-4/stack/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-5/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OTP-Servers-5 2 | Implement the *terminate* callback in your stack handler. Use IO.puts to report the arguments it receives. 3 | 4 | Try various ways of terminating your server. For example, popping an empty stack will raise an exception. You might add code that calls *System.halt(n)* if the *push* handler receives a number less than 10. (This will let you generate different return codes.) Use your imagination to try different scenarios. 5 | 6 | ## Solution 7 | See the [stack](./stack) directory for the full project. 8 | 9 | When the server shuts down due to an error, I don't see the *terminate* method being called. 10 | ``` 11 | iex> Stack.Server.start_link([1]) 12 | {:ok, #PID<0.113.0>} 13 | iex> Stack.Server.pop 14 | 1 15 | iex> Stack.Server.pop 16 | ** (EXIT from #PID<0.110.0>) an exception was raised: 17 | ** (MatchError) no match of right hand side value: [] 18 | (stack) lib/stack/server.ex:27: Stack.Server.handle_call/3 19 | (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4 20 | (stdlib) gen_server.erl:647: :gen_server.handle_msg/5 21 | (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3 22 | 23 | Interactive Elixir (1.2.6) - press Ctrl+C to exit (type h() ENTER for help) 24 | iex> 25 | 19:00:02.644 [error] GenServer MyStack terminating 26 | ** (MatchError) no match of right hand side value: [] 27 | (stack) lib/stack/server.ex:27: Stack.Server.handle_call/3 28 | (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4 29 | (stdlib) gen_server.erl:647: :gen_server.handle_msg/5 30 | (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3 31 | Last message: :pop 32 | State: [] 33 | ``` 34 | 35 | This piece of code was added as recommended in the problem; it will halt the system with an exit code of 0 if any integer less than 10 is pushed. 36 | ``` 37 | if is_integer(item) and item < 10 do 38 | System.halt(0) 39 | end 40 | ``` 41 | 42 | Note that when it runs, *terminate* is not called, the system immediately exits. 43 | ``` 44 | iex> Stack.Server.start_link([]) 45 | {:ok, #PID<0.125.0>} 46 | iex> Stack.Server.push(1) 47 | ``` 48 | 49 | However, I added this code as well to return a *:stop* when an integer of exactly 10 is pushed. 50 | ``` 51 | if is_integer(item) and item == 10 do 52 | {:stop, :normal, state} 53 | else 54 | {:noreply, [item | state]} 55 | end 56 | ``` 57 | 58 | This does trigger the *terminate* function: 59 | ``` 60 | iex> Stack.Server.start_link([]) 61 | {:ok, #PID<0.112.0>} 62 | iex> Stack.Server.push(10) 63 | :ok 64 | Reason: :normal; State: [] 65 | ``` 66 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-5/stack/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-5/stack/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-5/stack/lib/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack do 2 | end 3 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-5/stack/lib/stack/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Server do 2 | @name MyStack 3 | 4 | use GenServer 5 | 6 | def push(item) do 7 | GenServer.cast(@name, {:push, item}) 8 | end 9 | 10 | def pop do 11 | GenServer.call(@name, :pop) 12 | end 13 | 14 | ### 15 | # GenServer API 16 | ### 17 | 18 | def start_link(state) do 19 | GenServer.start_link(__MODULE__, state, [name: @name]) 20 | end 21 | 22 | def handle_cast({:push, item}, state) do 23 | if is_integer(item) and item < 10 do 24 | System.halt(0) 25 | end 26 | 27 | if is_integer(item) and item == 10 do 28 | {:stop, :normal, state} 29 | else 30 | {:noreply, [item | state]} 31 | end 32 | end 33 | 34 | def handle_call(:pop, _from, state) do 35 | [item | tail] = state 36 | {:reply, item, tail} 37 | end 38 | 39 | def terminate(reason, state) do 40 | IO.puts "Reason: #{inspect reason}; State: #{inspect state}" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-5/stack/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Stack.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :stack, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger]] 15 | end 16 | 17 | defp deps do 18 | [] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-5/stack/test/stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StackTest do 2 | use ExUnit.Case 3 | doctest Stack 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch16/OTP-Servers-5/stack/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OTP-Supervisors-1 2 | Add a supervisor to your stack application. Use *iex* to make sure it starts the server correctly. Use the server normally, and then crash it (try popping from an empty stack). Did it restart? What were the stack contents after the restart? 3 | 4 | ## Solution 5 | See the [stack](./stack) directory for the full project. 6 | 7 | The server restarted when fourth pop was called. When the supervisor restarted it, it was restarted with the same parameters as the first time, so the first pop return the integer 1. 8 | ``` 9 | iex> Stack.Server.pop 10 | 1 11 | iex> Stack.Server.pop 12 | 2 13 | iex> Stack.Server.pop 14 | 3 15 | iex> Stack.Server.pop 16 | Reason: {{:badmatch, []}, [{Stack.Server, :handle_call, 3, [file: 'lib/stack/server.ex', line: 35]}, {:gen_server, :try_handle_call, 4, [file: 'gen_server.erl', line: 615]}, {:gen_server, :handle_msg, 5, [file: 'gen_server.erl', line: 647]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 247]}]}; State: [] 17 | ** (exit) exited in: GenServer.call(MyStack, :pop, 5000) 18 | ** (EXIT) an exception was raised: 19 | ** (MatchError) no match of right hand side value: [] 20 | (stack) lib/stack/server.ex:35: Stack.Server.handle_call/3 21 | (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4 22 | (stdlib) gen_server.erl:647: :gen_server.handle_msg/5 23 | (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3 24 | 25 | 22:10:13.662 [error] GenServer MyStack terminating 26 | ** (MatchError) no match of right hand side value: [] 27 | (stack) lib/stack/server.ex:35: Stack.Server.handle_call/3 28 | (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4 29 | (stdlib) gen_server.erl:647: :gen_server.handle_msg/5 30 | (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3 31 | Last message: :pop 32 | State: [] 33 | (elixir) lib/gen_server.ex:564: GenServer.call/3 34 | iex> Stack.Server.pop 35 | 1 36 | ``` 37 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-1/stack/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-1/stack/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-1/stack/lib/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | import Supervisor.Spec, warn: false 6 | 7 | children = [ 8 | worker(Stack.Server, [[1, 2, 3]]), 9 | ] 10 | 11 | opts = [strategy: :one_for_one, name: Stack.Supervisor] 12 | Supervisor.start_link(children, opts) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-1/stack/lib/stack/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Server do 2 | @name MyStack 3 | 4 | use GenServer 5 | 6 | def push(item) do 7 | GenServer.cast(@name, {:push, item}) 8 | end 9 | 10 | def pop do 11 | GenServer.call(@name, :pop) 12 | end 13 | 14 | ### 15 | # GenServer API 16 | ### 17 | 18 | def start_link(state) do 19 | GenServer.start_link(__MODULE__, state, [name: @name]) 20 | end 21 | 22 | def handle_cast({:push, item}, state) do 23 | if is_integer(item) and item < 10 do 24 | System.halt(0) 25 | end 26 | 27 | if is_integer(item) and item == 10 do 28 | {:stop, :normal, state} 29 | else 30 | {:noreply, [item | state]} 31 | end 32 | end 33 | 34 | def handle_call(:pop, _from, state) do 35 | [item | tail] = state 36 | {:reply, item, tail} 37 | end 38 | 39 | def terminate(reason, state) do 40 | IO.puts "Reason: #{inspect reason}; State: #{inspect state}" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-1/stack/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Stack.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :stack, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger], 15 | mod: {Stack, []}] 16 | end 17 | 18 | defp deps do 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-1/stack/test/stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StackTest do 2 | use ExUnit.Case 3 | doctest Stack 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-1/stack/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/lib/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | {:ok, _pid} = Stack.StashSupervisor.start_link([1, 2, 3]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/lib/stack/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Server do 2 | @name MyStack 3 | 4 | use GenServer 5 | 6 | def push(item) do 7 | GenServer.cast(@name, {:push, item}) 8 | end 9 | 10 | def pop do 11 | GenServer.call(@name, :pop) 12 | end 13 | 14 | ### 15 | # GenServer API 16 | ### 17 | 18 | def start_link(stash_pid) do 19 | GenServer.start_link(__MODULE__, stash_pid, [name: @name]) 20 | end 21 | 22 | def init(stash_pid) do 23 | current_stack = Stack.Stash.get(stash_pid) 24 | {:ok, {current_stack, stash_pid}} 25 | end 26 | 27 | def handle_cast({:push, item}, {current_stack, stash_pid} = state) do 28 | case item do 29 | 1 -> 30 | raise RuntimeError 31 | 2 -> 32 | {:stop, :normal, state} 33 | _ -> 34 | {:noreply, {[item | current_stack], stash_pid}} 35 | end 36 | end 37 | 38 | def handle_call(:pop, _from, {current_stack, stash_pid}) do 39 | [item | tail] = current_stack 40 | {:reply, item, {tail, stash_pid}} 41 | end 42 | 43 | def terminate(reason, {current_stack, stash_pid} = state) do 44 | IO.puts "Stack.Server.terminate - Reason: #{inspect reason}; State: #{inspect state}" 45 | Stack.Stash.set(stash_pid, current_stack) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/lib/stack/stack_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.StackSupervisor do 2 | use Supervisor 3 | 4 | def start_link(stash_pid) do 5 | {:ok, _pid} = Supervisor.start_link(__MODULE__, stash_pid) 6 | end 7 | def init(stash_pid) do 8 | child_processes = [worker(Stack.Server, [stash_pid])] 9 | supervise(child_processes, strategy: :one_for_one) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/lib/stack/stash.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Stash do 2 | use GenServer 3 | 4 | def get(pid) do 5 | GenServer.call(pid, :get) 6 | end 7 | 8 | def set(pid, stack) do 9 | GenServer.cast(pid, {:set, stack}) 10 | end 11 | 12 | ### 13 | # GenServer API 14 | ### 15 | 16 | def start_link(initial_stack) do 17 | {:ok, _pid} = GenServer.start_link(__MODULE__, initial_stack) 18 | end 19 | 20 | def handle_call(:get, _from, current) do 21 | {:reply, current, current} 22 | end 23 | 24 | def handle_cast({:set, new}, _current) do 25 | {:noreply, new} 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/lib/stack/stash_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.StashSupervisor do 2 | use Supervisor 3 | 4 | def start_link(initial_stack) do 5 | result = {:ok, sup} = Supervisor.start_link(__MODULE__, [initial_stack]) 6 | start_workers(sup, initial_stack) 7 | result 8 | end 9 | def start_workers(sup, initial_stack) do 10 | {:ok, stash} = Supervisor.start_child(sup, worker(Stack.Stash, [initial_stack])) 11 | Supervisor.start_child(sup, supervisor(Stack.StackSupervisor, [stash])) 12 | end 13 | def init(_) do 14 | supervise([], strategy: :one_for_one) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Stack.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :stack, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger], 15 | mod: {Stack, []}] 16 | end 17 | 18 | defp deps do 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/test/stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StackTest do 2 | use ExUnit.Case 3 | doctest Stack 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch17/OTP-Supervisors-2/stack/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OTP-Applications-1 2 | Turn your stack server into an OTP application. 3 | 4 | ## Solution 5 | It already is! :) 6 | 7 | See the most recent [stack](../../ch17/OTP-Supervisors-2/stack/) implementation. More specifically, in the [mix.exs](../../ch17/OTP-Supervisors-2/stack/mix.exs) file, see the *application* function: 8 | ``` 9 | def application do 10 | [applications: [:logger], 11 | mod: {Stack, []}] 12 | end 13 | ``` 14 | 15 | This was automatically generated by mix when we created the project. 16 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OTP-Applications-2 2 | So far, we haven't written any tests for the application. Is there anything you can test? See what you can do. 3 | 4 | ## Solution 5 | See the [stack](./stack) directory for the full project. 6 | 7 | First we can add some tests for the *Stash* module to make sure setting and getting the value in the stash works: 8 | ``` 9 | defmodule StashTest do 10 | use ExUnit.Case 11 | 12 | test "get retrieves the value from the stash" do 13 | {:ok, pid} = Stack.Stash.start_link([1, 2, 3]) 14 | 15 | stack = Stack.Stash.get(pid) 16 | assert stack == [1, 2, 3] 17 | end 18 | 19 | test "set stores a value in the stash" do 20 | {:ok, pid} = Stack.Stash.start_link([]) 21 | 22 | Stack.Stash.set(pid, [1, 2, 3]) 23 | 24 | stack = Stack.Stash.get(pid) 25 | assert stack == [1, 2, 3] 26 | end 27 | end 28 | ``` 29 | 30 | A *Stash* process will actually be started automatically by the application but because we didn't name it we don't know its PID. So, we can just create a new one in the tests to use. 31 | 32 | Then we can add a couple tests to gain confidence that pushing and popping work: 33 | ``` 34 | defmodule ServerTest do 35 | use ExUnit.Case 36 | 37 | test "popping retrieves an item from the stack" do 38 | top = Stack.Server.pop 39 | assert top == 1 40 | end 41 | 42 | test "pushing adds an item to the stack" do 43 | Stack.Server.push(25) 44 | top = Stack.Server.pop 45 | assert top == 25 46 | end 47 | end 48 | ``` 49 | 50 | Running *mix test*: 51 | ``` 52 | $ mix test 53 | ..... 54 | 55 | Finished in 0.03 seconds (0.03s on load, 0.00s on tests) 56 | 5 tests, 0 failures 57 | 58 | Randomized with seed 389487 59 | ``` 60 | 61 | Obviously this is a pretty weak set of tests but it's better than what we had. 62 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/lib/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | {:ok, _pid} = Stack.StashSupervisor.start_link([1, 2, 3]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/lib/stack/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Server do 2 | @name MyStack 3 | 4 | use GenServer 5 | 6 | def push(item) do 7 | GenServer.cast(@name, {:push, item}) 8 | end 9 | 10 | def pop do 11 | GenServer.call(@name, :pop) 12 | end 13 | 14 | ### 15 | # GenServer API 16 | ### 17 | 18 | def start_link(stash_pid) do 19 | GenServer.start_link(__MODULE__, stash_pid, [name: @name]) 20 | end 21 | 22 | def init(stash_pid) do 23 | current_stack = Stack.Stash.get(stash_pid) 24 | {:ok, {current_stack, stash_pid}} 25 | end 26 | 27 | def handle_cast({:push, item}, {current_stack, stash_pid} = state) do 28 | case item do 29 | 1 -> 30 | raise RuntimeError 31 | 2 -> 32 | {:stop, :normal, state} 33 | _ -> 34 | {:noreply, {[item | current_stack], stash_pid}} 35 | end 36 | end 37 | 38 | def handle_call(:pop, _from, {current_stack, stash_pid}) do 39 | [item | tail] = current_stack 40 | {:reply, item, {tail, stash_pid}} 41 | end 42 | 43 | def terminate(reason, {current_stack, stash_pid} = state) do 44 | IO.puts "Stack.Server.terminate - Reason: #{inspect reason}; State: #{inspect state}" 45 | Stack.Stash.set(stash_pid, current_stack) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/lib/stack/stack_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.StackSupervisor do 2 | use Supervisor 3 | 4 | def start_link(stash_pid) do 5 | {:ok, _pid} = Supervisor.start_link(__MODULE__, stash_pid) 6 | end 7 | def init(stash_pid) do 8 | child_processes = [worker(Stack.Server, [stash_pid])] 9 | supervise(child_processes, strategy: :one_for_one) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/lib/stack/stash.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.Stash do 2 | use GenServer 3 | 4 | def get(pid) do 5 | GenServer.call(pid, :get) 6 | end 7 | 8 | def set(pid, stack) do 9 | GenServer.cast(pid, {:set, stack}) 10 | end 11 | 12 | ### 13 | # GenServer API 14 | ### 15 | 16 | def start_link(initial_stack) do 17 | {:ok, _pid} = GenServer.start_link(__MODULE__, initial_stack) 18 | end 19 | 20 | def handle_call(:get, _from, current) do 21 | {:reply, current, current} 22 | end 23 | 24 | def handle_cast({:set, new}, _current) do 25 | {:noreply, new} 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/lib/stack/stash_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack.StashSupervisor do 2 | use Supervisor 3 | 4 | def start_link(initial_stack) do 5 | result = {:ok, sup} = Supervisor.start_link(__MODULE__, [initial_stack]) 6 | start_workers(sup, initial_stack) 7 | result 8 | end 9 | def start_workers(sup, initial_stack) do 10 | {:ok, stash} = Supervisor.start_child(sup, worker(Stack.Stash, [initial_stack])) 11 | Supervisor.start_child(sup, supervisor(Stack.StackSupervisor, [stash])) 12 | end 13 | def init(_) do 14 | supervise([], strategy: :one_for_one) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Stack.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :stack, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger], 15 | mod: {Stack, []}] 16 | end 17 | 18 | defp deps do 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/test/server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ServerTest do 2 | use ExUnit.Case 3 | 4 | test "popping retrieves an item from the stack" do 5 | top = Stack.Server.pop 6 | assert top == 1 7 | end 8 | 9 | test "pushing adds an item to the stack" do 10 | Stack.Server.push(25) 11 | top = Stack.Server.pop 12 | assert top == 25 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/test/stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StackTest do 2 | use ExUnit.Case 3 | doctest Stack 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/test/stash_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StashTest do 2 | use ExUnit.Case 3 | 4 | test "get retrieves the value from the stash" do 5 | {:ok, pid} = Stack.Stash.start_link([1, 2, 3]) 6 | 7 | stack = Stack.Stash.get(pid) 8 | assert stack == [1, 2, 3] 9 | end 10 | 11 | test "set stores a value in the stash" do 12 | {:ok, pid} = Stack.Stash.start_link([]) 13 | 14 | Stack.Stash.set(pid, [1, 2, 3]) 15 | 16 | stack = Stack.Stash.get(pid) 17 | assert stack == [1, 2, 3] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-2/stack/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: OTP-Applications-3 2 | Our boss notices that after we applied our version-0-to-version-1 code change, the delta indeed works as specified. However, she also notices that if the server crashes, the delta is forgotten -- only the current number is retained. Write a new update that stashes both values. 3 | 4 | ## Solution 5 | See the [sequence](./sequence) directory for the full project. 6 | 7 | So, to accomplish this, we're not actually changing the sequence server's state struct at all. There are really only three necessary code changes. We want to store the delta when we terminate: 8 | ``` 9 | def terminate(_reason, state) do 10 | Sequence.Stash.save_value(state.stash_pid, {state.current_number, state.delta}) 11 | end 12 | ``` 13 | 14 | And we want to load the delta when we start: 15 | ``` 16 | def init(stash_pid) do 17 | {current_number, delta} = Sequence.Stash.get_value(stash_pid) 18 | {:ok, %State{current_number: current_number, stash_pid: stash_pid, delta: delta}} 19 | end 20 | ``` 21 | 22 | Lastly, for future runs, we will need to initialize the *Stash* with a delta of 1: 23 | ``` 24 | def start(_type, _args) do 25 | {:ok, _pid} = Sequence.Supervisor.start_link({123, 1}) 26 | end 27 | ``` 28 | 29 | I'm not sure if this is necessary, but the *code_change* function just needs to set the new state using the values from the old state: 30 | ``` 31 | def code_change("1", old_state = %{current_number: current_number, stash_pid: stash_pid, delta: delta}, _extra) do 32 | new_state = %State{current_number: current_number, stash_pid: stash_pid, delta: delta} 33 | Sequence.Stash.save_value(stash_pid, {current_number, delta}) 34 | Logger.info "Changing code from 1 to 2" 35 | Logger.info inspect(old_state) 36 | Logger.info inspect(new_state) 37 | {:ok, new_state} 38 | end 39 | ``` 40 | 41 | I wanted to be explicit, but we could probably just write it like: 42 | ``` 43 | def code_change("1", old_state, _extra) do 44 | new_state = old_state 45 | Sequence.Stash.save_value(stash_pid, {new_state.current_number, new_state.delta}) 46 | Logger.info "Changing code from 1 to 2" 47 | Logger.info inspect(old_state) 48 | Logger.info inspect(new_state) 49 | {:ok, new_state} 50 | end 51 | ``` 52 | 53 | Everything's looking good; let's see if after we apply the code change it maintains the delta across crashes: 54 | ``` 55 | iex> Sequence.Server.next_number 56 | 123 57 | iex> Sequence.Server.next_number 58 | 124 59 | iex> Sequence.Server.increment_number(5) 60 | :ok 61 | iex> Sequence.Server.next_number 62 | 130 63 | iex> :sys.suspend(Sequence.Server) 64 | :ok 65 | iex> c("new_server.ex") 66 | new_server.ex:1: warning: redefining module Sequence.Server 67 | new_server.ex:5: warning: redefining module Sequence.Server.State 68 | [Sequence.Server, Sequence.Server.State] 69 | iex> :sys.change_code(Sequence.Server, Sequence.Server, "1", []) 70 | 71 | 11:00:34.235 [info] Changing code from 1 to 2 72 | 73 | 11:00:34.238 [info] %Sequence.Server.State{current_number: 135, delta: 5, stash_pid: #PID<0.136.0>} 74 | 75 | 11:00:34.238 [info] %Sequence.Server.State{current_number: 135, delta: 5, stash_pid: #PID<0.136.0>} 76 | :ok 77 | iex> :sys.resume(Sequence.Server) 78 | :ok 79 | iex> Sequence.Server.next_number 80 | 135 81 | iex> Sequence.Server.increment_number("abc") 82 | :ok 83 | iex> 84 | 11:01:14.052 [error] GenServer Sequence.Server terminating 85 | ** (ArithmeticError) bad argument in arithmetic expression 86 | (sequence) new_server.ex:43: Sequence.Server.handle_cast/2 87 | (stdlib) gen_server.erl:601: :gen_server.try_dispatch/4 88 | (stdlib) gen_server.erl:667: :gen_server.handle_msg/5 89 | (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3 90 | Last message: {:"$gen_cast", {:increment_number, "abc"}} 91 | State: %Sequence.Server.State{current_number: 140, delta: 5, stash_pid: #PID<0.136.0>} 92 | 93 | nil 94 | iex> Sequence.Server.next_number 95 | 140 96 | iex> Sequence.Server.next_number 97 | 145 98 | ``` 99 | 100 | Radical. 101 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/lib/sequence.ex: -------------------------------------------------------------------------------- 1 | defmodule Sequence do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | #{:ok, _pid} = Sequence.Supervisor.start_link({123, 1}) 6 | {:ok, _pid} = Sequence.Supervisor.start_link(123) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/lib/sequence/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Sequence.Server do 2 | use GenServer 3 | require Logger 4 | 5 | defmodule State, do: defstruct current_number: 0, stash_pid: nil, delta: 1 6 | 7 | @vsn "1" 8 | 9 | ##### 10 | # External API 11 | 12 | def start_link(stash_pid) do 13 | {:ok, _pid} = GenServer.start_link(__MODULE__, stash_pid, name: __MODULE__) 14 | end 15 | 16 | def next_number do 17 | GenServer.call(__MODULE__, :next_number) 18 | end 19 | 20 | def increment_number(delta) do 21 | GenServer.cast(__MODULE__, {:increment_number, delta}) 22 | end 23 | 24 | ##### 25 | # GenServer implementation 26 | 27 | def init(stash_pid) do 28 | current_number = Sequence.Stash.get_value(stash_pid) 29 | {:ok, %State{current_number: current_number, stash_pid: stash_pid}} 30 | end 31 | 32 | def handle_call(:next_number, _from, state) do 33 | { 34 | :reply, 35 | state.current_number, 36 | %{state | current_number: state.current_number + state.delta} 37 | } 38 | end 39 | 40 | def handle_cast({:increment_number, delta}, state) do 41 | { 42 | :noreply, 43 | %{state | current_number: state.current_number + delta, delta: delta} 44 | } 45 | end 46 | 47 | def terminate(_reason, state) do 48 | Sequence.Stash.save_value(state.stash_pid, state.current_number) 49 | end 50 | 51 | def code_change("0", old_state = {current_number, stash_pid}, _extra) do 52 | new_state = %State{current_number: current_number, stash_pid: stash_pid, delta: 1} 53 | Logger.info "Changing code from 0 to 1" 54 | Logger.info inspect(old_state) 55 | Logger.info inspect(new_state) 56 | {:ok, new_state} 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/lib/sequence/stash.ex: -------------------------------------------------------------------------------- 1 | defmodule Sequence.Stash do 2 | use GenServer 3 | 4 | ##### 5 | # External API 6 | 7 | def start_link(current_value) do 8 | {:ok, _pid} = GenServer.start_link(__MODULE__, current_value) 9 | end 10 | 11 | def save_value(pid, value) do 12 | GenServer.cast(pid, {:save_value, value}) 13 | end 14 | 15 | def get_value(pid) do 16 | GenServer.call(pid, :get_value) 17 | end 18 | 19 | ##### 20 | # GenServer implementation 21 | 22 | def handle_call(:get_value, _from, current_value) do 23 | {:reply, current_value, current_value} 24 | end 25 | 26 | def handle_cast({:save_value, value}, _current_value) do 27 | {:noreply, value} 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/lib/sequence/subsupervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Sequence.SubSupervisor do 2 | use Supervisor 3 | 4 | def start_link(stash_pid) do 5 | {:ok, _pid} = Supervisor.start_link(__MODULE__, stash_pid) 6 | end 7 | def init(stash_pid) do 8 | child_processes = [worker(Sequence.Server, [stash_pid])] 9 | supervise(child_processes, strategy: :one_for_one) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/lib/sequence/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Sequence.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(initial_number) do 5 | result = {:ok, sup} = Supervisor.start_link(__MODULE__, [initial_number]) 6 | start_workers(sup, initial_number) 7 | result 8 | end 9 | 10 | def start_workers(sup, initial_number) do 11 | {:ok, stash} = Supervisor.start_child(sup, worker(Sequence.Stash, [initial_number])) 12 | Supervisor.start_child(sup, supervisor(Sequence.SubSupervisor, [stash])) 13 | end 14 | def init(_) do 15 | supervise([], strategy: :one_for_one) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Sequence.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :sequence, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger], 15 | mod: {Sequence, []}] 16 | end 17 | 18 | defp deps do 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/new_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Sequence.Server do 2 | use GenServer 3 | require Logger 4 | 5 | defmodule State, do: defstruct current_number: 0, stash_pid: nil, delta: 1 6 | 7 | @vsn "2" 8 | 9 | ##### 10 | # External API 11 | 12 | def start_link(stash_pid) do 13 | {:ok, _pid} = GenServer.start_link(__MODULE__, stash_pid, name: __MODULE__) 14 | end 15 | 16 | def next_number do 17 | GenServer.call(__MODULE__, :next_number) 18 | end 19 | 20 | def increment_number(delta) do 21 | GenServer.cast(__MODULE__, {:increment_number, delta}) 22 | end 23 | 24 | ##### 25 | # GenServer implementation 26 | 27 | def init(stash_pid) do 28 | %{current_number: current_number, delta: delta} = Sequence.Stash.get_value(stash_pid) 29 | {:ok, %State{current_number: current_number, stash_pid: stash_pid, delta: delta}} 30 | end 31 | 32 | def handle_call(:next_number, _from, state) do 33 | { 34 | :reply, 35 | state.current_number, 36 | %{state | current_number: state.current_number + state.delta} 37 | } 38 | end 39 | 40 | def handle_cast({:increment_number, delta}, state) do 41 | { 42 | :noreply, 43 | %{state | current_number: state.current_number + delta, delta: delta} 44 | } 45 | end 46 | 47 | def terminate(_reason, state) do 48 | value = %{current_number: state.current_number, delta: state.delta} 49 | Sequence.Stash.save_value(state.stash_pid, value) 50 | end 51 | 52 | def code_change("1", old_state = %{current_number: current_number, stash_pid: stash_pid, delta: delta}, _extra) do 53 | new_state = %State{current_number: current_number, stash_pid: stash_pid, delta: delta} 54 | Sequence.Stash.save_value(stash_pid, {current_number, delta}) 55 | Logger.info "Changing code from 1 to 2" 56 | Logger.info inspect(old_state) 57 | Logger.info inspect(new_state) 58 | {:ok, new_state} 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/test/sequence_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SequenceTest do 2 | use ExUnit.Case 3 | doctest Sequence 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /ch18/OTP-Applications-3/sequence/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /ch20/MacrosAndCodeEvaluation-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: MacrosAndCodeEvaluation-1 2 | Write a macro called *myunless* that implements the standard *unless* functionality. You're allowed to use the regular *if* expression in it. 3 | 4 | ## Solution 5 | See the [my.exs](./my.exs) file for the full module. 6 | 7 | This is almost a direct copy of the *if* macro so I won't bother describing the implementation. Testing the module in *iex*: 8 | ``` 9 | iex> require My 10 | nil 11 | iex> My.unless 1 == 2 do IO.puts "1 != 2" else IO.puts "1 == 2" end 12 | 1 != 2 13 | :ok 14 | iex> My.unless 1 == 1 do IO.puts "1 != 1" else IO.puts "1 == 1" end 15 | 1 == 1 16 | :ok 17 | ``` 18 | -------------------------------------------------------------------------------- /ch20/MacrosAndCodeEvaluation-1/my.exs: -------------------------------------------------------------------------------- 1 | defmodule My do 2 | defmacro unless(condition, clauses) do 3 | do_clause = Keyword.get(clauses, :do, nil) 4 | else_clause = Keyword.get(clauses, :else, nil) 5 | 6 | quote do 7 | case unquote(condition) do 8 | val when val in [false, nil] -> unquote(do_clause) 9 | _ -> unquote(else_clause) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /ch20/MacrosAndCodeEvaluation-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: MacrosAndCodeEvaluation-2 2 | Write a macro called *times_n* that takes a single numeric argument. It should define a function called *times_n* in the caller's module that itself takes a single argument, and that multiplies the argument by *n*. So, calling *times_n(3)* should create a function called *times_3*, and calling *times_3(4)* should return 12. Here's an example of it in use: 3 | 4 | ``` 5 | defmodule Test do 6 | require Times 7 | Times.times_n(3) 8 | Times.times_n(4) 9 | end 10 | 11 | IO.puts Test.times_3(4) #=> 12 12 | IO.puts Test.times_4(5) #=> 20 13 | ``` 14 | 15 | ## Solution 16 | See the [times.exs](./times.exs) file for the full module. 17 | 18 | The first thing we need to do is create the function name the macro will define. We can use string interpolation; however, if we just *unquote* the string, we will actually get a *CompileError*. The macro would essentially generate the following: 19 | ``` 20 | def "times_3"(x) do ... 21 | ``` 22 | 23 | So, let's convert the string to an atom: 24 | ``` 25 | defmodule Times do 26 | defmacro times_n(n) do 27 | fun = String.to_atom("times_#{n}") 28 | 29 | # TODO 30 | end 31 | end 32 | ``` 33 | 34 | Next, we simply write what the macro should do to define the function. The whole thing looks like: 35 | ``` 36 | defmodule Times do 37 | defmacro times_n(n) do 38 | fun = String.to_atom("times_#{n}") 39 | 40 | quote do 41 | def unquote(fun)(x) do 42 | x * unquote(n) 43 | end 44 | end 45 | end 46 | end 47 | ``` 48 | 49 | Using the same *Test* module as was defined in the problem, let's test it in *iex*: 50 | ``` 51 | iex> Test.times_3(4) 52 | 12 53 | iex> Test.times_4(5) 54 | 20 55 | ``` 56 | -------------------------------------------------------------------------------- /ch20/MacrosAndCodeEvaluation-2/times.exs: -------------------------------------------------------------------------------- 1 | defmodule Times do 2 | defmacro times_n(n) do 3 | fun = String.to_atom("times_#{n}") 4 | 5 | quote do 6 | def unquote(fun)(x) do 7 | x * unquote(n) 8 | end 9 | end 10 | end 11 | end 12 | 13 | defmodule Test do 14 | require Times 15 | Times.times_n(3) 16 | Times.times_n(4) 17 | end 18 | -------------------------------------------------------------------------------- /ch20/MacrosAndCodeEvaluation-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: MacrosAndCodeEvaluation-3 2 | The Elixir test framework, *ExUnit*, uses some clever code-quoting tricks. For example, if you assert 3 | ``` 4 | assert 5 < 4 5 | ``` 6 | You'll get the eror "expected 5 to be less than 4." 7 | 8 | The Elixir source code is on GitHub (at *https://github.com/elixir-lang/elixir*). The implementation of this is in the file *elixir/lib/ex_unit/assertions.ex*. Spend some time reading this file, and work out how it implements this trick. 9 | 10 | (Hard) Once you've done that, see if you can use the same technique to implement a function that takes an arbitrary arithmetic expression and returns a natural language version. 11 | ``` 12 | explain do: 2 + 3 * 4 13 | #=> multiply 3 and 4, then add 2 14 | ``` 15 | 16 | ## Solution 17 | See the [arithmetic.exs](./arithmetic.exs) file for the full module. 18 | 19 | To start off, I'm just going to get a better feel for the code respresentation of arithmetic expressions. Let's define a macro that will just print the expression: 20 | ``` 21 | defmodule Arithmetic do 22 | defmacro print(clause) do 23 | IO.puts inspect clause 24 | end 25 | ``` 26 | 27 | And let's print some: 28 | ``` 29 | iex> require Arithmetic 30 | nil 31 | iex> Arithmetic.print 1+2 32 | {:+, [line: 2], [1, 2]} 33 | :ok 34 | iex> Arithmetic.print 1+2 * 7 35 | {:+, [line: 3], [1, {:*, [line: 3], [2, 7]}]} 36 | :ok 37 | iex> Arithmetic.print 1 + 2 - 3 / 4 * 7 38 | {:-, [line: 4], [{:+, [line: 4], [1, 2]}, {:*, [line: 4], [{:/, [line: 4], [3, 4]}, 7]}]} 39 | :ok 40 | ``` 41 | 42 | Let's take that last one and format it a little better so we can think about what it means: 43 | ``` 44 | {:-, [], [ 45 | {:+, [], [1, 2]}, 46 | {:*, [], [ 47 | {:/, [], [3, 4]}, 48 | 7 49 | ]} 50 | ]} 51 | ``` 52 | 53 | Aha, so we basically have a tree where the deeper into the tree we get, the higher priority the operation is. Given an operation, there will be 4 cases we need to handle: 54 | - Both operands are ints 55 | - The left operand is an int but the right is a tuple (A sub-operation) 56 | - The right operand is an int but the left is a tuple (A sub-operation) 57 | - Both operands are tuples 58 | 59 | We will need to be able to translate three types of strings: 60 | - Two integer operands (E.g. multiply 1 and 2) 61 | - One integer operand (E.g. then add 4) 62 | - No integer opernds (E.g. then add the results) 63 | 64 | Putting it all together: 65 | ``` 66 | defp do_explain({op, _, [a, b]}, nil) when is_integer(a) and is_integer(b), 67 | do: do_translate(op, a, b) 68 | 69 | defp do_explain({op, _, [a, b]}, acc) when is_integer(a) and is_integer(b), 70 | do: acc <> ", then #{do_translate(op, a, b)}" 71 | 72 | defp do_explain({op, _, [a, b]}, acc) when is_integer(a) and is_tuple(b), 73 | do: do_explain(b, acc) <> do_translate(op, a) 74 | 75 | defp do_explain({op, _, [a, b]}, acc) when is_tuple(a) and is_integer(b), 76 | do: do_explain(a, acc) <> do_translate(op, b) 77 | 78 | defp do_explain({op, _, [a, b]}, acc) when is_tuple(a) and is_tuple(b) do 79 | acc = do_explain(a, acc) 80 | acc = do_explain(b, acc) 81 | acc <> do_translate(op) 82 | end 83 | 84 | defp do_translate(:+, a, b) when is_integer(a) and is_integer(b), do: "add #{a} to #{b}" 85 | defp do_translate(:-, a, b) when is_integer(a) and is_integer(b), do: "subtract #{b} from #{a}" 86 | defp do_translate(:*, a, b) when is_integer(a) and is_integer(b), do: "multiply #{a} by #{b}" 87 | defp do_translate(:/, a, b) when is_integer(a) and is_integer(b), do: "divide #{a} by #{b}" 88 | 89 | defp do_translate(:+, x) when is_integer(x), do: ", then add #{x}" 90 | defp do_translate(:-, x) when is_integer(x), do: ", then subtract by #{x}" 91 | defp do_translate(:*, x) when is_integer(x), do: ", then multiply by #{x}" 92 | defp do_translate(:/, x) when is_integer(x), do: ", then divide by #{x}" 93 | 94 | defp do_translate(:+), do: ", then add the results" 95 | defp do_translate(:-), do: ", then the second result from the first" 96 | defp do_translate(:*), do: ", then multiply the results" 97 | defp do_translate(:/), do: ", then divide the first result by the second" 98 | ``` 99 | 100 | The actual macro is really simple: 101 | ``` 102 | defmacro explain(clauses) do 103 | do_clause = Keyword.get(clauses, :do, nil) 104 | do_explain(do_clause, nil) 105 | end 106 | ``` 107 | 108 | Neat! 109 | -------------------------------------------------------------------------------- /ch20/MacrosAndCodeEvaluation-3/arithmetic.exs: -------------------------------------------------------------------------------- 1 | defmodule Arithmetic do 2 | defmacro explain(clauses) do 3 | do_clause = Keyword.get(clauses, :do, nil) 4 | do_explain(do_clause, nil) 5 | end 6 | 7 | defp do_explain({op, _, [a, b]}, nil) when is_integer(a) and is_integer(b), 8 | do: do_translate(op, a, b) 9 | 10 | defp do_explain({op, _, [a, b]}, acc) when is_integer(a) and is_integer(b), 11 | do: acc <> ", then #{do_translate(op, a, b)}" 12 | 13 | defp do_explain({op, _, [a, b]}, acc) when is_integer(a) and is_tuple(b), 14 | do: do_explain(b, acc) <> do_translate(op, a) 15 | 16 | defp do_explain({op, _, [a, b]}, acc) when is_tuple(a) and is_integer(b), 17 | do: do_explain(a, acc) <> do_translate(op, b) 18 | 19 | defp do_explain({op, _, [a, b]}, acc) when is_tuple(a) and is_tuple(b) do 20 | acc = do_explain(a, acc) 21 | acc = do_explain(b, acc) 22 | acc <> do_translate(op) 23 | end 24 | 25 | defp do_translate(:+, a, b) when is_integer(a) and is_integer(b), do: "add #{a} to #{b}" 26 | defp do_translate(:-, a, b) when is_integer(a) and is_integer(b), do: "subtract #{b} from #{a}" 27 | defp do_translate(:*, a, b) when is_integer(a) and is_integer(b), do: "multiply #{a} by #{b}" 28 | defp do_translate(:/, a, b) when is_integer(a) and is_integer(b), do: "divide #{a} by #{b}" 29 | 30 | defp do_translate(:+, x) when is_integer(x), do: ", then add #{x}" 31 | defp do_translate(:-, x) when is_integer(x), do: ", then subtract by #{x}" 32 | defp do_translate(:*, x) when is_integer(x), do: ", then multiply by #{x}" 33 | defp do_translate(:/, x) when is_integer(x), do: ", then divide by #{x}" 34 | 35 | defp do_translate(:+), do: ", then add the results" 36 | defp do_translate(:-), do: ", then the second result from the first" 37 | defp do_translate(:*), do: ", then multiply the results" 38 | defp do_translate(:/), do: ", then divide the first result by the second" 39 | end 40 | 41 | defmodule Test do 42 | require Arithmetic 43 | 44 | def test do 45 | IO.puts "Explaining: 1 + 2 * 3" 46 | sentence = Arithmetic.explain do: 1 + 2 * 3 47 | IO.puts sentence 48 | 49 | IO.puts "Explaining: 1 * 2 + 3" 50 | sentence = Arithmetic.explain do: 1 * 2 + 3 51 | IO.puts sentence 52 | 53 | IO.puts "Explaining: 1 * 2 + 3 * 4" 54 | sentence = Arithmetic.explain do: 1 * 2 + 3 * 4 55 | IO.puts sentence 56 | 57 | IO.puts "Explaining: 3 * 4 + 1 / 3 - 3 + 1" 58 | sentence = Arithmetic.explain do: 3 * 4 + 1 / 3 - 3 + 1 59 | IO.puts sentence 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /ch21/LinkingModules-BehavioursAndUse-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: LinkingModules-BehavioursAndUse-1 2 | In the body of the *def* macro, there's a quote block that defines the actual method. It contains 3 | ``` 4 | IO.puts "==> call: #{Tracer.dump_definition(unquote(name), unquote(args))}" 5 | result = unquote(content) 6 | IO.puts "<== result: #{result}" 7 | ``` 8 | Why does the first call to puts have to unquote the values in its interpolation but the second call does not? 9 | 10 | ## Solution 11 | The first call has to unquote the values because they were passed in to the macro and they need to be evaluated. The second call does not because the variable named result was actually given a value on the line prior and thus in the interpolation that variable exists as is. 12 | -------------------------------------------------------------------------------- /ch21/LinkingModules-BehavioursAndUse-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: LinkingModules-BehavioursAndUse-2 2 | The built-in function IO.ANSI.escape will insert ANSI escape sequences in a string. If you put the resulting strings into a terminal, you can a dd colors and bold or underlined text. Explore the library, and then use it to colorize our tracing's output. 3 | 4 | ## Solution 5 | See the [colorize.exs](./colorize.exs) file for the full module. 6 | 7 | I've looked through the 1.1, 1.2, and 1.3 docs and IO.ANSI definitely doesn't have an *escape* function; however, the module does contain a number of other coloration functions so I will use those instead. 8 | 9 | Lets make the calls blue and the results green: 10 | ``` 11 | IO.puts "#{IO.ANSI.blue}==> call: #{Tracer.dump_defn(unquote(name), unquote(args))}" 12 | result = unquote(content) 13 | IO.puts "#{IO.ANSI.green}<== result: #{result}" 14 | ``` 15 | 16 | I doubt colors will show up in markdown but it works! 17 | -------------------------------------------------------------------------------- /ch21/LinkingModules-BehavioursAndUse-2/colorize.exs: -------------------------------------------------------------------------------- 1 | defmodule Tracer do 2 | def dump_args(args) do 3 | args |> Enum.map(&inspect/1) |> Enum.join(", ") 4 | end 5 | 6 | def dump_defn(name, args) do 7 | "#{name}(#{dump_args(args)})" 8 | end 9 | 10 | defmacro def(definition={name,_,args}, do: content) do 11 | quote do 12 | Kernel.def(unquote(definition)) do 13 | IO.puts "#{IO.ANSI.blue}==> call: #{Tracer.dump_defn(unquote(name), unquote(args))}" 14 | result = unquote(content) 15 | IO.puts "#{IO.ANSI.green}<== result: #{result}" 16 | end 17 | end 18 | end 19 | 20 | defmacro __using__(_opts) do 21 | quote do 22 | import Kernel, except: [def: 2] 23 | import unquote(__MODULE__), only: [def: 2] 24 | end 25 | end 26 | end 27 | 28 | defmodule Test do 29 | use Tracer 30 | 31 | def puts_sum_three(a,b,c), do: IO.inspect(a+b+c) 32 | def add_list(list), do: Enum.reduce(list, 0, &(&1 + &2)) 33 | end 34 | 35 | Test.puts_sum_three(1, 2, 3) 36 | Test.add_list([5, 7, 8]) 37 | -------------------------------------------------------------------------------- /ch21/LinkingModules-BehavioursAndUse-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: LinkingModules-BehavioursAndUse-3 2 | (Hard) Try adding a method definition with a guard clause to the *Test* module. You'll find that the tracing no longer works. 3 | - Find out why. 4 | - See if you can fix it. 5 | 6 | ## Solution 7 | See the [tracer.exs](./tracer.exs) file for the full module. 8 | 9 | I'm going to start off by printing what the arguments now look like. Change the macro to this temporarily: 10 | ``` 11 | defmacro def(definition, do: content) do 12 | IO.puts inspect definition 13 | end 14 | ``` 15 | Let's use it in a basic function with a single guard clause: 16 | ``` 17 | def increment(x) when is_integer(x), do: x + 1 18 | ``` 19 | And see what it outputs: 20 | ``` 21 | {:when, [line: 38], [{:increment, [line: 38], [{:x, [line: 38], nil}]}, {:is_integer, [line: 38], [{:x, [line: 38], nil}]}]} 22 | ``` 23 | Reformatted for readability: 24 | ``` 25 | {:when, [line: 38], 26 | [ 27 | {:increment, [line: 38], [{:x, [line: 38], nil}]}, 28 | {:is_integer, [line: 38], [{:x, [line: 38], nil}]} 29 | ]} 30 | ``` 31 | 32 | Aha, so it looks like *:when* is an operation where the second operand is a condition and the first operand is the operation to execute if the condition is met. Let's see if this changes with an additional guard clause: 33 | ``` 34 | def increment_by(x, y) when is_integer(x) and is_integer(y), do: x + y 35 | ``` 36 | The formatted output: 37 | ``` 38 | {:when, [line: 39], 39 | [ 40 | {:increment_by, [line: 39], [{:x, [line: 39], nil}]}, 41 | {:and, [line: 39], 42 | [ 43 | {:is_integer, [line: 39], [{:x, [line: 39], nil}]}, 44 | {:is_integer, [line: 39], [{:y, [line: 39], nil}]} 45 | ]} 46 | ]} 47 | ``` 48 | Cool! Even though it appears we added an "additional" guard clause, it's just an extension of the same clause using *and*. Let's try to account for a possible guard clause: 49 | ``` 50 | defmacro def({:when,_,[{name,_,args} = definition,_]}, do: content) do 51 | quote do 52 | Kernel.def(unquote(definition)) do 53 | IO.puts "#{IO.ANSI.blue}==> call: #{Tracer.dump_defn(unquote(name), unquote(args))}" 54 | result = unquote(content) 55 | IO.puts "#{IO.ANSI.green}<== result: #{result}" 56 | end 57 | end 58 | end 59 | ``` 60 | 61 | I am making the assumption that the arguments for *when* will always be ordered correctly. 62 | 63 | Let's test it out: 64 | ``` 65 | iex> Test.increment_by(1,2) 66 | ==> call: increment_by(1, 2) 67 | <== result: 3 68 | :ok 69 | iex> Test.increment(2) 70 | ==> call: increment(2) 71 | <== result: 3 72 | :ok 73 | ``` 74 | 75 | Nice. This makes me excited for the *Metaprogramming Elixir* book which is next on my reading list! 76 | -------------------------------------------------------------------------------- /ch21/LinkingModules-BehavioursAndUse-3/tracer.exs: -------------------------------------------------------------------------------- 1 | defmodule Tracer do 2 | def dump_args(args) do 3 | args |> Enum.map(&inspect/1) |> Enum.join(", ") 4 | end 5 | 6 | def dump_defn(name, args) do 7 | "#{name}(#{dump_args(args)})" 8 | end 9 | 10 | # This version will account for a guard clause being present. 11 | defmacro def({:when,_,[{name,_,args} = definition,_]}, do: content) do 12 | quote do 13 | Kernel.def(unquote(definition)) do 14 | IO.puts "#{IO.ANSI.blue}==> call: #{Tracer.dump_defn(unquote(name), unquote(args))}" 15 | result = unquote(content) 16 | IO.puts "#{IO.ANSI.green}<== result: #{result}" 17 | end 18 | end 19 | end 20 | 21 | defmacro def(definition={name,_,args}, do: content) do 22 | quote do 23 | Kernel.def(unquote(definition)) do 24 | IO.puts "#{IO.ANSI.blue}==> call: #{Tracer.dump_defn(unquote(name), unquote(args))}" 25 | result = unquote(content) 26 | IO.puts "#{IO.ANSI.green}<== result: #{result}" 27 | end 28 | end 29 | end 30 | 31 | defmacro __using__(_opts) do 32 | quote do 33 | import Kernel, except: [def: 2] 34 | import unquote(__MODULE__), only: [def: 2] 35 | end 36 | end 37 | end 38 | 39 | defmodule Test do 40 | use Tracer 41 | 42 | #def puts_sum_three(a,b,c), do: IO.inspect(a+b+c) 43 | #def add_list(list), do: Enum.reduce(list, 0, &(&1 + &2)) 44 | 45 | def increment(x) when is_integer(x), do: x + 1 46 | def increment_by(x, y) when is_integer(x) and is_integer(y), do: x + y 47 | end 48 | 49 | #Test.puts_sum_three(1, 2, 3) 50 | #Test.add_list([5, 7, 8]) 51 | 52 | #Test.add(1, 2) 53 | -------------------------------------------------------------------------------- /ch22/Protocols-1/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: Protocols-1 2 | A basic Caesar cypher consists of shifting the letters in a message by a fixed offset. For an offset of 1, for example, a will become b, b will become c, and z will become a. If the offset is 13, we have the ROT13 algorithm. 3 | 4 | Lists and binaries can both be *stringlike*. Write a *Caesar* protocol that applies to both. It would include two functions: *encrypt(string, shift)* and *rot13(string)*. 5 | 6 | ## Solution 7 | See the [caesar.exs](./caesar.exs) file for the full solution. 8 | 9 | I actually started off by writing what it would look like to use the protocol. Over the past few days I read the first ~200 pages of *Elixir in Action*, and the author uses this technique a lot (Writing the expected usage of a module before writing the module) and I like it. So, here's how I expect the module to look: 10 | ``` 11 | string = "Hello, world!" 12 | IO.puts "Original string: \"#{string}\"" 13 | IO.puts "Shifted by 3: #{Encryptable.encrypt(string, 3)}" 14 | IO.puts "Shifted by 13 (ROT13): #{Encryptable.rot13(string)}" 15 | 16 | list = String.to_char_list(string) 17 | IO.puts "Original char list: \"#{list}\"" 18 | IO.puts "Shifted by 3: #{Encryptable.encrypt(list, 3)}" 19 | IO.puts "Shifted by 13 (ROT13): #{Encryptable.rot13(list)}" 20 | ``` 21 | 22 | First we create a regular string (binary) and print it's actual value followed by the result of shifting the characters by three and thirteen. Then we make a character list out of the binary string and do the same thing. 23 | 24 | So, here's what the protocol looks like: 25 | ``` 26 | defprotocol Encryptable do 27 | def encrypt(string, shift) 28 | def rot13(string) 29 | end 30 | ``` 31 | 32 | Pretty dang simple and easy to interpret. Let's do the string implementation first since it will be identical to the character list implementation with some additional conversions to make the string simpler to parse. To my knowledge, Strings don't implement the Enumerable protocol the way lists and maps do, so we first convert the string to a character list. Then, we can use *Enum.map* which will, for every character, call a separate function that will shift the specific character. 33 | ``` 34 | defimpl Encryptable, for: BitString do 35 | def encrypt(string, shift) do 36 | string 37 | |> String.to_char_list 38 | |> Enum.map(&Char.shift(&1, shift)) 39 | |> List.to_string 40 | end 41 | 42 | def rot13(string) do 43 | encrypt(string, 13) 44 | end 45 | end 46 | ``` 47 | 48 | Note that for the *ROT13* function, we can just utilize the encrypt method instead of duplicating any code. Let's take a quick look at the *Char* module we need in order to shift each character: 49 | ``` 50 | defmodule Char do 51 | def shift(char, shift) when char < 0x5b and char > 0x40 do 52 | case char + shift do 53 | new_char when new_char >= 0x5b -> new_char - 26 54 | new_char when new_char <= 0x40 -> new_char + 26 55 | new_char -> new_char 56 | end 57 | end 58 | 59 | def shift(char, shift) when char < 0x7b and char > 0x60 do 60 | case char + shift do 61 | new_char when new_char >= 0x7b -> new_char - 26 62 | new_char when new_char <= 0x60 -> new_char + 26 63 | new_char -> new_char 64 | end 65 | end 66 | 67 | def shift(char, _), do: char 68 | end 69 | ``` 70 | This should also be pretty self explanatory, apart from the magical numbers in the guard clauses. Those numbers are just the ASCII ranges of a-z and A-Z. We only want to shift letters, not punctuation. In the function bodies, we either return the raw shifted value, or if the raw shifted value moved outside the acceptable range, we rotate it back in. (There is a bug in here if shift is greater than 26 but that could be easily addressed with an additional check in the guard clauses.) 71 | 72 | Now that our string implementation is done and the helper module is written, let's implement the list version. As previously stated, the list version is actually the same exact logic, minus the conversions which are now unnecessary: 73 | ``` 74 | defimpl Encryptable, for: List do 75 | def encrypt(string, shift) do 76 | string 77 | |> Enum.map(&Char.shift(&1, shift)) 78 | end 79 | 80 | def rot13(string) do 81 | encrypt(string, 13) 82 | end 83 | end 84 | ``` 85 | 86 | Running it in *iex* we get: 87 | ``` 88 | $ iex caesar.exs 89 | 90 | Original string: "Hello, world!" 91 | Shifted by 3: Khoor, zruog! 92 | Shifted by 13 (ROT13): Uryyb, jbeyq! 93 | Original char list: "Hello, world!" 94 | Shifted by 3: Khoor, zruog! 95 | Shifted by 13 (ROT13): Uryyb, jbeyq! 96 | ``` 97 | -------------------------------------------------------------------------------- /ch22/Protocols-1/caesar.exs: -------------------------------------------------------------------------------- 1 | defprotocol Encryptable do 2 | def encrypt(string, shift) 3 | def rot13(string) 4 | end 5 | 6 | defimpl Encryptable, for: BitString do 7 | def encrypt(string, shift) do 8 | string 9 | |> String.to_char_list 10 | |> Enum.map(&Char.shift(&1, shift)) 11 | |> List.to_string 12 | end 13 | 14 | def rot13(string) do 15 | encrypt(string, 13) 16 | end 17 | end 18 | 19 | defimpl Encryptable, for: List do 20 | def encrypt(string, shift) do 21 | string 22 | |> Enum.map(&Char.shift(&1, shift)) 23 | end 24 | 25 | def rot13(string) do 26 | encrypt(string, 13) 27 | end 28 | end 29 | 30 | defmodule Char do 31 | def shift(char, shift) when char < 0x5b and char > 0x40 do 32 | case char + shift do 33 | new_char when new_char >= 0x5b -> new_char - 26 34 | new_char when new_char <= 0x40 -> new_char + 26 35 | new_char -> new_char 36 | end 37 | end 38 | 39 | def shift(char, shift) when char < 0x7b and char > 0x60 do 40 | case char + shift do 41 | new_char when new_char >= 0x7b -> new_char - 26 42 | new_char when new_char <= 0x60 -> new_char + 26 43 | new_char -> new_char 44 | end 45 | end 46 | 47 | def shift(char, _), do: char 48 | end 49 | 50 | string = "Hello, world!" 51 | IO.puts "Original string: \"#{string}\"" 52 | IO.puts "Shifted by 3: #{Encryptable.encrypt(string, 3)}" 53 | IO.puts "Shifted by 13 (ROT13): #{Encryptable.rot13(string)}" 54 | 55 | list = String.to_char_list(string) 56 | IO.puts "Original char list: \"#{list}\"" 57 | IO.puts "Shifted by 3: #{Encryptable.encrypt(list, 3)}" 58 | IO.puts "Shifted by 13 (ROT13): #{Encryptable.rot13(list)}" 59 | -------------------------------------------------------------------------------- /ch22/Protocols-2/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: Protocols-2 2 | Using a list of words in your language, write a program to look for words where the result of calling *rot13(word)* is also a word in the list. (For various English word lists, look at [http://wordlist.sourceforge.net/]. The SCOWL collection looks promising, as it already has words divided by size.) 3 | 4 | ## Solution 5 | See the [caesar.exs](./caesar.exs) file for the full solution. 6 | 7 | Using SCOWL - as recommended in the problem stated - I generated a list of English words in the [words.txt](./words.txt) file. Before we start coding, let's come up with the algorithm we plan on using. I'm not going to try to optimize the runtime performance, but I want a game plan before I start the implementation. 8 | 9 | The easiest solution I am envisioning is: 10 | 1. Read the word list from the file, storing each word as an entry in a *MapSet*. 11 | 2. Iterate through the *MapSet* and for each word, rotate the word and then check if the rotation is a member of the *MapSet*. 12 | 3. If the element is a member, append it to a list. If the element is not a member, ignore it. 13 | 14 | Sounds simple enough. I don't know about the internal workings of the *MapSet* type but I would hope that lookups (Membership checks) are constant time. If that's the case, our algorithm will be O(n). 15 | 16 | Let's start out by reading the file into a *MapSet*: 17 | ``` 18 | words = "words.txt" 19 | |> File.stream! 20 | |> Stream.map(&String.strip(&1)) 21 | |> Enum.reduce(MapSet.new, fn(word, acc) -> MapSet.put(acc, word) end) 22 | ``` 23 | Ah, Elixir is truly beautiful. In case it isn't clear what's going on, we are: 24 | 1. Opening a file stream so that we don't read the entire file at once. 25 | 2. Stripping any leading or trailing white space off of each line (lazily). 26 | 3. Reducing the list of words into a single *MapSet* containing each word. Steps 1 and 2 were both lazy, so this is the only time we actually iterate through the list! 27 | 28 | Now that we have the list of words in memory, the next step was to iterate through each word, rotate the word, and check if the word exists in the original set. Let's do it: 29 | ``` 30 | defmodule Test do 31 | def test do 32 | words = "words.txt" 33 | |> File.stream! 34 | |> Stream.map(&String.strip(&1)) 35 | |> Enum.reduce(MapSet.new, fn(word, acc) -> MapSet.put(acc, word) end) 36 | 37 | words 38 | |> Enum.reduce([], fn(word, acc) -> update_results(word, words, acc) end) 39 | end 40 | 41 | def rotation_exists?(word, words) do 42 | rotated = Encryptable.rot13(word) 43 | MapSet.member?(words, rotated) 44 | end 45 | 46 | def update_results(word, words, acc) do 47 | case rotation_exists?(word, words) do 48 | true -> [word | acc] 49 | false -> acc 50 | end 51 | end 52 | end 53 | ``` 54 | 55 | So, first we create a simple function called *rotation_exists?*. It takes a word and a *MapSet* of words, applies ROT13 to the word, and then returns whether or not the *MapSet* of words contains the result. Next, we create a (Ambiguously named) function called *update_results*. This takes a word, a *MapSet* of words, and an accumulator. It checks if the rotation exists and adds the word to the accumulator if so. 56 | 57 | Finally, in the *test* method, we call *Enum.reduce* using the original set of words to create a new list containing only words whose rotations exist. 58 | 59 | Here it is in *iex*: 60 | ``` 61 | iex> Test.test 62 | ["or", "one", "rail", "be", "bar", "envy", "she", "fur"] 63 | ``` 64 | 65 | Let's check one of the results: 66 | ``` 67 | iex> Encryptable.rot13("rail") 68 | "envy" 69 | ``` 70 | That is a word and it make sense that "envy" happens to be in the list, as its rotation should be "rail". 71 | -------------------------------------------------------------------------------- /ch22/Protocols-2/caesar.exs: -------------------------------------------------------------------------------- 1 | defprotocol Encryptable do 2 | def encrypt(string, shift) 3 | def rot13(string) 4 | end 5 | 6 | defimpl Encryptable, for: BitString do 7 | def encrypt(string, shift) do 8 | string 9 | |> String.to_char_list 10 | |> Enum.map(&Char.shift(&1, shift)) 11 | |> List.to_string 12 | end 13 | 14 | def rot13(string) do 15 | encrypt(string, 13) 16 | end 17 | end 18 | 19 | defimpl Encryptable, for: List do 20 | def encrypt(string, shift) do 21 | string 22 | |> Enum.map(&Char.shift(&1, shift)) 23 | end 24 | 25 | def rot13(string) do 26 | encrypt(string, 13) 27 | end 28 | end 29 | 30 | defmodule Char do 31 | def shift(char, shift) when char < 0x5b and char > 0x40 do 32 | case char + shift do 33 | new_char when new_char >= 0x5b -> new_char - 26 34 | new_char when new_char <= 0x40 -> new_char + 26 35 | new_char -> new_char 36 | end 37 | end 38 | 39 | def shift(char, shift) when char < 0x7b and char > 0x60 do 40 | case char + shift do 41 | new_char when new_char >= 0x7b -> new_char - 26 42 | new_char when new_char <= 0x60 -> new_char + 26 43 | new_char -> new_char 44 | end 45 | end 46 | 47 | def shift(char, _), do: char 48 | end 49 | 50 | defmodule Test do 51 | def test do 52 | words = "words.txt" 53 | |> File.stream! 54 | |> Stream.map(&String.strip(&1)) 55 | |> Enum.reduce(MapSet.new, fn(word, acc) -> MapSet.put(acc, word) end) 56 | 57 | words 58 | |> Enum.reduce([], fn(word, acc) -> update_results(word, words, acc) end) 59 | |> IO.inspect 60 | end 61 | 62 | def rotation_exists?(word, words) do 63 | rotated = Encryptable.rot13(word) 64 | MapSet.member?(words, rotated) 65 | end 66 | 67 | def update_results(word, words, acc) do 68 | case rotation_exists?(word, words) do 69 | true -> [word | acc] 70 | false -> acc 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /ch22/Protocols-3/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: Protocols-3 2 | Collections that implement the *Enumerable* protocol define *count*, *member?*, and *reduce* functions. The *Enum* module uses these to implement methods such as *each*, *filter*, and *map*. 3 | 4 | Implement your own versions of *each*, *filter*, and *map* in terms of reduce. 5 | 6 | ## Solution 7 | See the [my_enum.exs](./my_enum.exs) file for the full solution. 8 | 9 | Let's start with *each*. We actually won't be reducing the enumerable at all. All we care about is running a function on each element and returning *:ok*. Here's what that looks like: 10 | ``` 11 | def each(enumerable, fun) do 12 | reducer = fn(elem, _acc) -> 13 | fun.(elem) 14 | {:cont, nil} 15 | end 16 | 17 | enumerable 18 | |> Enumerable.reduce({:cont, nil}, reducer) 19 | 20 | :ok 21 | end 22 | ``` 23 | The accumulator doesn't really matter, apart from the *:cont* atom which tells *reduce* to continue iterating, so we can just set it to and leave it as *nil*. Our reducer just runs the function that was provided on the element and at the end we return *:ok*. 24 | 25 | Next let's look at filter. This one is a little less intuitive as we aren't really filtering the enumerable. We are actually just constructing a new list and adding only the elements that meet the condition defined by the provided function: 26 | ``` 27 | def filter(enumerable, fun) do 28 | reducer = fn(elem, acc) -> 29 | case fun.(elem) do 30 | true -> {:cont, [elem | acc]} 31 | false -> {:cont, acc} 32 | end 33 | end 34 | 35 | {:done, result} = enumerable 36 | |> Enumerable.reduce({:cont, []}, reducer) 37 | 38 | result 39 | |> Enum.reverse 40 | end 41 | ``` 42 | So, the *reducer* just prepends or ignores each element based on the result of the provided function. At the end we reverse the result to put the list back into its original order. (I cheated a little bit by using the *Enum.reverse* function but the problem statement wasn't to write that ourself.) 43 | 44 | Finally, let's look at map. This one is simpler than *filter* in my opinion; it's practically the same logic except we don't care about the provided function's return value. Instead, we just always prepend the result to the resulting list: 45 | ``` 46 | def map(enumerable, fun) do 47 | reducer = fn(elem, acc) -> 48 | {:cont, [fun.(elem) | acc]} 49 | end 50 | 51 | {:done, result} = enumerable 52 | |> Enumerable.reduce({:cont, []}, reducer) 53 | 54 | result 55 | |> Enum.reverse 56 | end 57 | ``` 58 | 59 | Let's test our implementation using the following module: 60 | ``` 61 | defmodule Test do 62 | def test_each do 63 | [1, 2, 3, 4, 5] 64 | |> MyEnum.each(&(IO.puts &1)) 65 | end 66 | 67 | def test_filter do 68 | [1, 2, 3, 4, 5] 69 | |> MyEnum.filter(&(&1 > 3)) 70 | end 71 | 72 | def test_map do 73 | [1, 2, 3, 4, 5] 74 | |> MyEnum.map(&(&1 * &1)) 75 | end 76 | end 77 | ``` 78 | 79 | In *iex*: 80 | ``` 81 | iex> Test.test_each 82 | 1 83 | 2 84 | 3 85 | 4 86 | 5 87 | :ok 88 | iex> Test.test_filter 89 | [4, 5] 90 | iex> Test.test_map 91 | [1, 4, 9, 16, 25] 92 | ``` 93 | 94 | Looks good! 95 | -------------------------------------------------------------------------------- /ch22/Protocols-3/my_enum.exs: -------------------------------------------------------------------------------- 1 | defmodule MyEnum do 2 | def each(enumerable, fun) do 3 | reducer = fn(elem, _acc) -> 4 | fun.(elem) 5 | {:cont, nil} 6 | end 7 | 8 | enumerable 9 | |> Enumerable.reduce({:cont, nil}, reducer) 10 | 11 | :ok 12 | end 13 | 14 | def filter(enumerable, fun) do 15 | reducer = fn(elem, acc) -> 16 | case fun.(elem) do 17 | true -> {:cont, [elem | acc]} 18 | false -> {:cont, acc} 19 | end 20 | end 21 | 22 | {:done, result} = enumerable 23 | |> Enumerable.reduce({:cont, []}, reducer) 24 | 25 | result 26 | |> Enum.reverse 27 | end 28 | 29 | def map(enumerable, fun) do 30 | reducer = fn(elem, acc) -> 31 | {:cont, [fun.(elem) | acc]} 32 | end 33 | 34 | {:done, result} = enumerable 35 | |> Enumerable.reduce({:cont, []}, reducer) 36 | 37 | result 38 | |> Enum.reverse 39 | end 40 | end 41 | 42 | defmodule Test do 43 | def test_each do 44 | [1, 2, 3, 4, 5] 45 | |> MyEnum.each(&(IO.puts &1)) 46 | end 47 | 48 | def test_filter do 49 | [1, 2, 3, 4, 5] 50 | |> MyEnum.filter(&(&1 > 3)) 51 | end 52 | 53 | def test_map do 54 | [1, 2, 3, 4, 5] 55 | |> MyEnum.map(&(&1 * &1)) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /ch22/Protocols-4/README.md: -------------------------------------------------------------------------------- 1 | # Exercise: Protocols-4 2 | In many cases, *inspect* will return a valid Elixir literal for the value being inspected. Update the *inspect* function for structs so that it returns valid Elixir code to construct a new struct equal to the value being inspected. 3 | 4 | ## Solution 5 | See the [todo.exs](./todo.exs) file for the full solution. 6 | 7 | TODO 8 | --------------------------------------------------------------------------------