├── .gitignore ├── .tool-versions ├── LICENSE ├── README.md ├── chapter_10 └── exercise_1 │ ├── Makefile │ ├── sample_app.erl │ └── test │ └── test.erl ├── chapter_12 ├── README.md ├── exercise_1 │ └── spawn_registered_fun.erl ├── exercise_2 │ ├── 30000_graph.png │ ├── processes_graph.erl │ ├── spawn_times_10000.gnuplot │ └── spawn_times_30000.gnuplot └── exercise_3 │ └── process_ring.erl ├── chapter_13 ├── README.md ├── exercise_1 │ └── keep_alive.erl ├── exercise_2 │ └── on_exit.erl ├── exercise_3 │ └── timed_process.erl ├── exercise_4 │ └── monitored_process.erl ├── exercise_5 │ └── worker_supervisor.erl ├── exercise_6 │ └── worker_supervisor.erl └── spawn_monitored.erl ├── chapter_14 ├── README.md └── exercise_4 │ ├── lib_chan.conf │ ├── lib_chan │ ├── lib_chan.erl │ ├── lib_chan_auth.erl │ ├── lib_chan_cs.erl │ ├── lib_chan_mm.erl │ ├── lib_chan_test.erl │ └── lib_md5.erl │ ├── shared_files │ └── afile.txt │ └── yaf_server.erl ├── chapter_15 ├── README.md └── exercise_3 │ └── cpu_info.erl ├── chapter_16 ├── README.md ├── exercise_1 │ └── stale_beam ├── exercise_2 │ └── emd5sum ├── exercise_3 │ └── emd5sum ├── exercise_4 │ └── find_duplicate_jpegs ├── exercise_5 │ └── md5_cache.erl └── exercise_6 │ └── tweet_store.erl ├── chapter_17 ├── README.md ├── exercise_1 │ └── nano_get_url.erl ├── exercise_2 │ ├── lib_misc.erl │ ├── nano_client.erl │ └── nano_server.erl ├── exercise_3 │ ├── lib_misc.erl │ ├── nano_client.erl │ └── nano_server.erl ├── exercise_4 │ ├── encrypt.erl │ ├── lib_misc.erl │ ├── nano_client.erl │ └── nano_server.erl └── exercise_5 │ ├── lib_misc.erl │ ├── nano_client.erl │ └── nano_server.erl ├── chapter_18 ├── README.md └── exercise_1 │ ├── .gitignore │ ├── jquery-1.7.1.min.js │ ├── rebar.config │ ├── rebar.lock │ ├── shell1.css │ ├── shell1.html │ ├── src │ ├── exercise_1.app.src │ ├── exercise_1_app.erl │ ├── rebar.lock │ ├── shell1.erl │ └── webserver.erl │ └── websock.js ├── chapter_19 ├── README.md ├── exercise_1 │ └── module_exports.erl ├── exercise_2 │ └── count.erl └── exercise_3 │ ├── plagiarism_detector │ └── text_files │ ├── genesis_1.txt │ ├── genesis_2.txt │ ├── genesis_2_excerpt.txt │ └── genesis_3.txt ├── chapter_2 ├── README.md ├── exercise_3 │ └── hello.erl └── exercise_4 │ ├── afile_client.erl │ └── afile_server.erl ├── chapter_20 ├── README.md ├── exercise_1 │ └── erlang_tips.erl ├── exercise_2 │ └── erlang_tips.erl └── exercise_3 │ └── erlang_tips.erl ├── chapter_21 ├── README.md ├── exercise_1 │ └── dict.erl ├── exercise_2 │ └── dict_test.erl ├── exercise_3 │ └── dict_test_coverage.erl └── exercise_4 │ └── dict_test_debug.erl ├── chapter_22 ├── README.md ├── exercise_1 │ └── job_centre.erl ├── exercise_2 │ └── job_centre.erl ├── exercise_3 │ └── job_centre.erl └── exercise_4 │ └── job_centre.erl ├── chapter_23 ├── README.md ├── exercise_1 │ ├── lib_lin.erl │ ├── lib_primes.erl │ ├── prime_tester_server.erl │ ├── sellaprime.app │ ├── sellaprime_app.erl │ └── sellaprime_supervisor.erl ├── exercise_2 │ ├── .gitignore │ ├── rebar.config │ ├── rebar.lock │ └── src │ │ ├── lib_lin.erl │ │ ├── lib_primes.erl │ │ ├── prime_tester_server.erl │ │ ├── prime_tester_server_test.erl │ │ ├── prime_tester_worker.erl │ │ ├── prime_tester_worker_sup.erl │ │ ├── sellaprime.app.src │ │ ├── sellaprime_app.erl │ │ └── sellaprime_supervisor.erl ├── exercise_3 │ ├── .gitignore │ ├── rebar.config │ ├── rebar.lock │ └── src │ │ ├── lib_lin.erl │ │ ├── lib_primes.erl │ │ ├── prime_tester_load_balancer.erl │ │ ├── prime_tester_server_test.erl │ │ ├── prime_tester_worker.erl │ │ ├── prime_tester_worker_sup.erl │ │ ├── sellaprime.app.src │ │ ├── sellaprime_app.erl │ │ └── sellaprime_supervisor.erl └── exercise_5 │ ├── .gitignore │ ├── rebar.config │ ├── rebar.lock │ ├── src │ ├── lib_lin.erl │ ├── lib_primes.erl │ ├── lib_tester_db.erl │ ├── prime_tester_load_balancer.erl │ ├── prime_tester_server_test.erl │ ├── prime_tester_worker.erl │ ├── prime_tester_worker_sup.erl │ ├── sellaprime.app.src │ ├── sellaprime_app.erl │ └── sellaprime_supervisor.erl │ └── watson │ └── rebar.lock ├── chapter_24 ├── README.md ├── exercise_1 │ ├── adapter_db1.erl │ └── adapter_db1_test.erl ├── exercise_2 │ ├── adapter_db1.erl │ └── adapter_db1_test.erl └── exercise_3 │ ├── adapter_db1.erl │ └── adapter_db1_test.erl ├── chapter_25 ├── README.md ├── exercise_2 │ ├── .gitignore │ ├── rebar.config │ ├── rebar.lock │ └── src │ │ ├── cgi_web_server.erl │ │ ├── exercise_2.app.src │ │ └── ping_pong.erl ├── exercise_3 │ ├── .gitignore │ ├── rebar.config │ ├── rebar.lock │ └── src │ │ ├── cgi_web_server.erl │ │ ├── exercise_2.app.src │ │ └── ping_pong.erl ├── exercise_4 │ ├── .gitignore │ ├── rebar.config │ ├── rebar.lock │ └── src │ │ ├── cgi_web_server.erl │ │ ├── exercise_4.app.src │ │ ├── ping_pong.erl │ │ └── rebar.lock ├── exercise_5 │ ├── .gitignore │ ├── rebar.config │ ├── rebar.lock │ └── src │ │ ├── cgi_web_server.erl │ │ ├── exercise_5.app.src │ │ ├── ping_pong.erl │ │ └── rebar.lock └── exercise_6 │ ├── .gitignore │ ├── rebar.config │ ├── rebar.lock │ └── src │ ├── cgi_web_server.erl │ ├── exercise_6.app.src │ ├── exercise_6.erl │ ├── ping_pong.erl │ └── rebar.lock ├── chapter_26 ├── README.md ├── exercise_1 │ └── web_profiler.erl ├── exercise_2 │ └── web_profiler.erl ├── exercise_3 │ └── web_profiler.erl ├── exercise_5 │ └── web_profiler.erl └── exercise_6 │ └── web_profiler.erl ├── chapter_3 └── README.md ├── chapter_4 ├── README.md ├── exercise_1 │ └── geometry.erl ├── exercise_2 │ └── my_tuple_to_list.erl ├── exercise_3 │ └── my_time_and_date.erl ├── exercise_4 │ └── python_datetime.erl ├── exercise_5 │ └── math_functions.erl ├── exercise_6 │ └── math_functions.erl └── exercise_7 │ └── math_functions.erl ├── chapter_5 ├── README.md ├── exercise_1 │ ├── Makefile │ ├── json_configuration.erl │ └── sample_config.json ├── exercise_2 │ └── map_search.erl └── exercise_3 │ └── hash.erl ├── chapter_6 ├── exercise_1 │ └── myfile.erl └── exercise_2 │ └── try_test.erl ├── chapter_7 ├── exercise_1 │ └── reverse_bytes.erl ├── exercise_2 │ └── term_to_packet.erl ├── exercise_3 │ └── packet_to_term.erl ├── exercise_4 │ └── packet_conversions.erl └── exercise_5 │ └── reverse_bits.erl ├── chapter_8 ├── README.md ├── exercise_1 │ └── dict_functions.erl └── exercise_2 │ └── module_functions.erl └── chapter_9 ├── README.md ├── exercise_1 └── simple_types.erl └── exercise_4 ├── opaque_record.erl └── opaque_violation.erl /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all .beam files 2 | *.beam 3 | *.COVER.out 4 | 5 | *Mnesia.* 6 | *.dump 7 | deps/* 8 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 20.3 2 | rebar 3.4.1 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Trevor Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Programming Erlang Exercises 2 | ============================ 3 | 4 | Stratus3D 5 | 6 | Solutions to the exercises in Joe Armstrong's book *[Programming Erlang](https://pragprog.com/book/jaerlang2/programming-erlang)* 2nd Edition. Solutions are my own. Better ones may exist. 7 | 8 | ## Index 9 | 10 | * Chapter 1 - no exercises 11 | * Chapter 2 - [exercises 3 and 4 in the chapter_2 directory](chapter_2/) 12 | * Chapter 3 - [README in the chapter_3 directory](chapter_3/) 13 | * Chapter 4 - [exercises 1, 2, 3, 4, 5, 6, and 7 in the chapter_4 directory](chapter_4/) 14 | * Chapter 5 - [exercises 1, 2, and 3 in the chapter_5 directory](chapter_5/) 15 | * Chapter 6 - [exercises 1 and 2 in the chapter_6 directory](chapter_6/) 16 | * Chapter 7 - [exercises 1, 2, 3, 4, and 5 in the chapter_7 directory](chapter_7/) 17 | * Chapter 8 - [exercises 1 and 2 in the chapter_8 directory](chapter_8/) 18 | * Chapter 9 - [exercises 1, 2, 3, and 4 in the chapter_9 directory](chapter_9/) 19 | * Chapter 10 - [exercise 1 in the chapter_10 directory](chapter_10/) 20 | * Chapter 11 - no exercises 21 | * Chapter 12 - [exercises 1, 2, and 3 in the chapter_12 directory](chapter_12/) 22 | * Chapter 13 - [exercises 1, 2, 3, 4, 5 and 6 in chapter_13 directory](chapter_13/) 23 | * Chapter 14 - [exercises 1, 2, 3 and 4 in chapter_14 directory](chapter_14/) 24 | * Chapter 15 - [exercises 3 in the chapter 15 directory](chapter_15/) 25 | * Chapter 16 - [exercises 1, 2, 3, 4, 5 and 6 in the chapter_16 directory](chapter_16/) 26 | * Chapter 17 - [exercises 1, 2, 3, 4 and 5 in the chapter_17 directory](chapter_17/) 27 | * Chapter 18 - [exercise 1 in the chapter_18 directory](chapter_18/) 28 | * Chapter 19 - [exercise 1, 2, and 3 in the chapter_19 directory](chapter_19/) 29 | * Chapter 20 - [exercises 1, 2, and 3 in the chapter_20 directory](chapter_20/) 30 | * Chapter 21 - [exercises 1, 2, 3, and 4 in the chapter_21 directory](chapter_21/) 31 | * Chapter 22 - [exercises 1, 2, 3, and 4 in the chapter_22 directory](chapter_22/) 32 | * Chapter 23 - [exercises 1, 2, 3, 4, 5 and 6 in the chapter_23 directory](chapter_23/) 33 | * Chapter 24 - [exercises 1, 2 and 3 in the chapter_24 directory](chapter_24/) 34 | * Chapter 25 - [exercises 2, 3, 4, 5, and 6 in the chapter_25 directory](chapter_25/) 35 | * Chapter 26 - [exercises 1, 2, 3, 5, and 6 in the chapter_26 directory](chapter_26/) 36 | * Chapter 27 - no exercises 37 | 38 | ## Issues 39 | If you see something that could be improved feel free to open an issue on GitHub ([https://github.com/Stratus3D/programming_erlang_exercises/issues](https://github.com/Stratus3D/programming_erlang_exercises/issues)) 40 | 41 | ## Contributing 42 | Feel free to create a pull request if you see something that could be improved. 43 | -------------------------------------------------------------------------------- /chapter_10/exercise_1/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tests 2 | 3 | .SUFFIXES: .erl .beam .yrl 4 | 5 | .erl.beam: 6 | erlc -W $< 7 | 8 | .yrl.erl: 9 | erlc -W $< 10 | 11 | ERL = erl -boot start_clean 12 | 13 | MODS = sample_app 14 | 15 | # List of all the test modules. test:start/0 is our entry point 16 | TEST_MODS = test 17 | 18 | all: compile 19 | ${ERL} -s sample_app start 20 | 21 | compile: ${MODS:%=%.beam} 22 | 23 | # Compile all the modules in the test directory, then run test:start/0, which 24 | # should start the tests 25 | tests: ${TEST_MODS:%=test/%.beam} 26 | ${ERL} -s test start 27 | 28 | clean: 29 | # Clean source 30 | rm -rf *.beam erl_crash.dump 31 | # Clean tests 32 | rm -rf test/*.beam 33 | -------------------------------------------------------------------------------- /chapter_10/exercise_1/sample_app.erl: -------------------------------------------------------------------------------- 1 | -module(sample_app). 2 | 3 | -export([start/0]). 4 | 5 | start() -> 6 | % Dummy start function 7 | ok. 8 | -------------------------------------------------------------------------------- /chapter_10/exercise_1/test/test.erl: -------------------------------------------------------------------------------- 1 | -module(test). 2 | 3 | -export([start/0]). 4 | 5 | start() -> 6 | io:format("Running tests...~n"), 7 | % Run tests here... 8 | io:format("Tests finished.~n"), 9 | init:stop(), 10 | ok. 11 | -------------------------------------------------------------------------------- /chapter_12/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 12 2 | 3 | **1. Write a function `start(AnAtom, Fun)` to register `AnAtom` as `spawn(Fun)`. Make sure the program works correctly in the case when two parallel processes simultaneously evalutate `start/2`. In this case ensure one process fails and the other succeeds.** 4 | 5 | In `exercise_1/` there is a module named `spawn_registered_fun` that contains a `start/2` function. 6 | 7 | Example usage: 8 | 9 | ``` 10 | erlc spawn_registered_fun.erl 11 | erl 12 | 1> spawn_registered_fun:start(foo, fun() -> receive _ -> ok end end). 13 | {ok,<0.40.0>} 14 | 2> spawn_registered_fun:start(foo, fun() -> receive _ -> ok end end). 15 | {error,already_running} 16 | ``` 17 | 18 | **2. Measure process spawning time on your machine. Use the program in Section 12.3 on page 189. Plot a graph of the number of process compared with process creation time. What can you deduce from the graph?** 19 | 20 | In `exercise_2/` there is a file named `processes_graph.erl`. This Erlang file will spawn processes and time the takes to spawn them. The module the file defines contains two public functions. `spawn_and_time/1` spawns N number of processes and returns the average time it took to spawn them. `generate_data/3` calls `spawn_and_time/1` with a different number until a certain number of proceses are created, and writes the resulting averages to a CSV file. 21 | 22 | Example usage: 23 | 24 | ``` 25 | erlc processes_graph.erl 26 | erl 27 | % Generate file for averages up 10,000 processes 28 | 1> processes_graph:generate_data("10000_processes.csv", 10000, 100). 29 | % Generate file for averages up 30,000 processes 30 | 2> processes_graph:generate_data("30000_processes.csv", 30000, 100). 31 | ``` 32 | 33 | Graph the data in the CSVs can be done with the gnuplot files I have already created: 34 | 35 | ``` 36 | $ gnuplot -p spawn_times_10000.gnuplot 37 | # generates graph of average runtimes for up to 10,000 processes 38 | $ gnuplot -p spawn_times_30000.gnuplot 39 | # generates graph of average runtimes for up to 30,000 processes 40 | ``` 41 | 42 | The graphs should look something like the one below 43 | 44 | ![30,000 Processes Graph](exercise_2/30000_graph.png) 45 | 46 | Looking at these graphs it is clear that the average spawn time for a single process is constant. Whether there is 1 process running or 10,000, spawning a new proceses takes about the same amount of time. 47 | 48 | **3. Write a ring benchmark. Create a N processes in a ring. Send a message around the ring M times so that N * M messages are sent. Time how long it takes for different values of N and M.** 49 | 50 | In `exercises_3/` there is a file named `process_ring.erl`. The file defines a module that exports a function named `run/3`. Invoking this function spawns a ring of processes and sends a message around the ring the number of times specified. The time it takes to spawn the ring and send the message around it is printed out by the function. 51 | 52 | Example usage: 53 | 54 | ``` 55 | $ erlc process_ring.erl 56 | $ erl 57 | 1> process_ring:run(1000, 1000, message). 58 | Spawning 1000 processes and sending the message around the ring 1000 times took 485268 microseconds 59 | stop 60 | ``` 61 | -------------------------------------------------------------------------------- /chapter_12/exercise_1/spawn_registered_fun.erl: -------------------------------------------------------------------------------- 1 | -module(spawn_registered_fun). 2 | 3 | -export([start/2]). 4 | 5 | % Example usage: 6 | % 7 | % 1> spawn_registered_fun:start(foo, fun() -> receive _ -> ok end end). 8 | % {ok,<0.40.0>} 9 | % 2> spawn_registered_fun:start(foo, fun() -> receive _ -> ok end end). 10 | % {error,already_running} 11 | 12 | % Use guards to ensure the arguments passed in are what this function requires 13 | start(AnAtom, Fun) when is_atom(AnAtom), is_function(Fun, 0) -> 14 | Sender = self(), 15 | 16 | % Since we can only execute Fun after we are sure the the register/2 call 17 | % has succeeded we need to invoke register/2 and then if it succeeds execute 18 | % Fun. This is most easily done inside the newly spawned process. If the 19 | % register/2 call fails we can simply send an error back to the spawning 20 | % process and let the spawned process exit normally. 21 | Runner = fun() -> 22 | try register(AnAtom, self()) of 23 | true -> 24 | Sender ! {ok, self()}, 25 | Fun() 26 | catch 27 | error:_ -> 28 | Sender ! {error, {already_running, self()}} 29 | end 30 | end, 31 | 32 | % Spawn the Runner function as a linked process so we are alware of errors 33 | Pid = spawn_link(Runner), 34 | 35 | % Receive the messages back from the Runner process so we know the status 36 | % of the register/2 call so we can return the appropriate value to the 37 | % caller of this function. 38 | receive 39 | {ok, Pid} -> {ok, Pid}; 40 | {error, {already_running, Pid}} -> {error, already_running} 41 | end. 42 | 43 | 44 | -------------------------------------------------------------------------------- /chapter_12/exercise_2/30000_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stratus3D/programming_erlang_exercises/e4fd01024812059d338facc20f551e7dff4dac7e/chapter_12/exercise_2/30000_graph.png -------------------------------------------------------------------------------- /chapter_12/exercise_2/processes_graph.erl: -------------------------------------------------------------------------------- 1 | -module(processes_graph). 2 | 3 | -export([generate_data/3, spawn_and_time/1]). 4 | 5 | % generate_data(OutputFilename, End, Step) 6 | % 7 | % Invoke spawn_and_time/1 repeatedly with an number that increases by `Step` 8 | % until `End` is reached. Write the resulting times to the file specified as 9 | % `OutputFilename` formatted as CSV. 10 | % 11 | % Example 12 | % processes_graph:generate_data("data.csv", 50, 25). 13 | 14 | generate_data(OutputFilename, End, Step) when is_integer(End), is_integer(Step) -> 15 | {ok, File} = file:open(OutputFilename, [write]), 16 | Start = 1, 17 | Steps = lists:seq(Start, End + 1, Step), 18 | io:format("Steps: ~p", [Steps]), 19 | 20 | Max = erlang:system_info(process_limit), 21 | io:format("Maximum allowed processes: ~p~n", [Max]), 22 | 23 | RunStats = fun(N) -> 24 | [T1, T2] = spawn_and_time(N), 25 | io:format(File, "~p, ~p, ~p~n", [N, T1, T2]) 26 | end, 27 | 28 | lists:foreach(RunStats, Steps), 29 | 30 | ok = file:close(File). 31 | 32 | 33 | % spawn_and_time(N) 34 | % 35 | % Create N processes then destroy them. See how much time this takes and return 36 | % the runtime and wall block time. 37 | 38 | spawn_and_time(N) -> 39 | statistics(runtime), 40 | statistics(wall_clock), 41 | L = for(1, N, fun() -> spawn(fun() -> wait() end) end), 42 | {_, Time1} = statistics(runtime), 43 | {_, Time2} = statistics(wall_clock), 44 | lists:foreach(fun(Pid) -> Pid ! die end, L), 45 | U1 = Time1 * 1000 / N, 46 | U2 = Time2 * 1000 / N, 47 | [U1, U2]. 48 | 49 | wait() -> 50 | receive 51 | die -> void 52 | end. 53 | 54 | for(N, N, F) -> [F()]; 55 | for(I, N, F) -> [F()|for(I+1, N, F)]. 56 | -------------------------------------------------------------------------------- /chapter_12/exercise_2/spawn_times_10000.gnuplot: -------------------------------------------------------------------------------- 1 | set datafile separator ',' 2 | set ylabel 'Milliseconds' 3 | set xlabel 'Number of Processes' 4 | plot "10000_processes.csv" using 1:2 with lines, '' using 1:3 with lines 5 | -------------------------------------------------------------------------------- /chapter_12/exercise_2/spawn_times_30000.gnuplot: -------------------------------------------------------------------------------- 1 | set datafile separator ',' 2 | set ylabel 'Milliseconds' 3 | set xlabel 'Number of Processes' 4 | plot "30000_processes.csv" using 1:2 with lines, '' using 1:3 with lines 5 | -------------------------------------------------------------------------------- /chapter_12/exercise_3/process_ring.erl: -------------------------------------------------------------------------------- 1 | -module(process_ring). 2 | 3 | -export([run/3, loop/1]). 4 | 5 | % Spawn a ring of `Processes` number processes and send `Message` around the 6 | % ring `Loops` times. 7 | % 8 | % Example usage: 9 | % > process_ring:run(1000, 1000, message). 10 | % Spawning 1000 processes and sending the message around the ring 1000 times took 485268 microseconds 11 | 12 | run(Processes, Loops, Message) when is_integer(Processes), is_integer(Loops) -> 13 | 14 | {Time, FirstPid} = timer:tc(fun() -> 15 | % Spawn processes in loop 16 | FirstPid = spawn_ring(Processes, self()), 17 | 18 | % Send process around the ring 19 | send_around_ring(Loops, FirstPid, Message), 20 | 21 | FirstPid 22 | end), 23 | 24 | % Print out the results 25 | FormatString = "Spawning ~p processes and sending the message around the ring ~p times took ~p milliseconds~n", 26 | io:format(FormatString, [Processes, Loops, Time]), 27 | 28 | % Tear down the ring 29 | FirstPid ! stop. 30 | 31 | send_around_ring(0, _FirstPid, _Message) -> 32 | ok; 33 | send_around_ring(Loops, FirstPid, Message) -> 34 | FirstPid ! Message, 35 | receive 36 | Message -> send_around_ring(Loops - 1, FirstPid, Message) 37 | end. 38 | 39 | spawn_ring(0, PreviousPid) -> 40 | PreviousPid; 41 | spawn_ring(Processes, PreviousPid) -> 42 | Pid = spawn_link(?MODULE, loop, [PreviousPid]), 43 | spawn_ring(Processes - 1, Pid). 44 | 45 | % Server loop for processes in ring 46 | loop(NextPid) -> 47 | receive 48 | stop -> 49 | NextPid ! stop, 50 | ok; 51 | Value -> 52 | NextPid ! Value, 53 | loop(NextPid) 54 | end. 55 | -------------------------------------------------------------------------------- /chapter_13/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 13 2 | 3 | **1. Write a function named `my_spawn/3` that spawns a process and prints a message when it dies.** 4 | 5 | Solution in [exercise_1/](exercise_1/). 6 | 7 | **2. Solve exercise 1 with the `on_exit` function shown in chapter 13.** 8 | 9 | Solution in [exercise_2/](exercise_2/). 10 | 11 | **3. Write a function named `my_spawn/4` that spawns a process and kills it after it runs for more than the given number of seconds.** 12 | 13 | Solution in [exercise_3/](exercise_3/). 14 | 15 | **4. Write a function that creates a registered process that prints "I'm still running" every 5 seconds. Create another function that monitors this function and restarts it if it dies.** 16 | 17 | Solution in [exercise_4/](exercise_4). 18 | 19 | **5. Write a function that spawns and monitors several processes. If any monitored processes dies, restart the monitored process.** 20 | 21 | Solution in [exercise_5/](exercise_5). The code is in `worker_supervisor.erl` 22 | 23 | Example usage: 24 | 25 | ``` 26 | erlc worker_supervisors.erl 27 | erl 28 | % Start the supervisor with a list of children 29 | 1> SupervisorPid = worker_supervisor:start(worker_supervisor:definition()). 30 | I'm still running 31 | I'm still running 32 | % Get one of the child processes 33 | 2> WorkerData = worker_supervisor:get_workers(SupervisorPid). 34 | [{<0.41.0>,#Ref<0.0.4.148>,-576460742360537501,{worker_supervisor,test,[]}}, 35 | {<0.42.0>,#Ref<0.0.4.149>,-576460742360529319,{worker_supervisor,test,[]}}] 36 | 3> [{Pid, _, _, _}|_] = WorkerData. 37 | % And kill it to verify it got restarted 38 | 4> exit(Pid, test). 39 | ``` 40 | 41 | **6. Write a function that spawns and monitors several processes. If any monitored processes dies, kill all the processes and restart them.** 42 | 43 | Solution in [exercise_6/](exercise_6). The code is in `worker_supervisor.erl` 44 | 45 | ``` 46 | erlc worker_supervisors.erl 47 | erl 48 | % Start the supervisor with a list of children 49 | 1> SupervisorPid = worker_supervisor:start(worker_supervisor:definition()). 50 | I'm still running 51 | I'm still running 52 | % Get one of the child processes 53 | 2> WorkerData = worker_supervisor:get_workers(SupervisorPid). 54 | [{<0.41.0>,#Ref<0.0.4.148>,-576460742360537501,{worker_supervisor,test,[]}}, 55 | {<0.42.0>,#Ref<0.0.4.149>,-576460742360529319,{worker_supervisor,test,[]}}] 56 | 3> [{Pid, _, _, _}|_] = WorkerData. 57 | % And kill it to verify it got restarted 58 | 4> exit(Pid, test). 59 | ``` 60 | -------------------------------------------------------------------------------- /chapter_13/exercise_1/keep_alive.erl: -------------------------------------------------------------------------------- 1 | -module(keep_alive). 2 | -export([test/0, spawn_and_restart/4, test_spawn_and_restart/0]). 3 | 4 | test() -> 5 | receive 6 | Msg -> 7 | io:write(Msg), 8 | test() 9 | after 10 | 5000 -> 11 | io:format("I'm still running~n"), 12 | test() 13 | end. 14 | 15 | spawn_and_restart(Module, Function, Args, Seconds) -> 16 | Pid = spawn(Module, Function, Args), 17 | StartTime = now(), 18 | 19 | spawn(fun() -> 20 | Ref = monitor(process, Pid), 21 | receive 22 | {'DOWN', Ref, process, Pid, Why} -> 23 | TimeDifference = calculate_time_difference(now(), StartTime), 24 | io:format("==================================================~n"), 25 | io:format("Process ~w died in ~w millisconds with reason: ~w~n", [Pid, TimeDifference, Why]), 26 | ?MODULE:spawn_and_restart(Module, Function, Args, Seconds), 27 | io:format("Process Restarted~n"), 28 | io:format("==================================================~n"), 29 | exit(self(), kill) 30 | end 31 | end), 32 | Pi. 33 | 34 | calculate_time_difference(Current, Past) -> 35 | {Mega, Secs, _} = Current, 36 | {PastMega, PastSecs, _} = Past, 37 | (Mega * 1000000 + Secs) - (PastMega * 1000000 + PastSecs). 38 | 39 | test_spawn_and_restart() -> 40 | Pid = ?MODULE:spawn_and_restart(?MODULE, test, [], 30), 41 | receive 42 | something -> 43 | blah 44 | after 45 | 20000 -> 46 | exit(Pid, kill) 47 | end. 48 | -------------------------------------------------------------------------------- /chapter_13/exercise_2/on_exit.erl: -------------------------------------------------------------------------------- 1 | -module(on_exit). 2 | -export([test/0, spawn_and_print_crash/3, test_spawn_and_print_crash/0]). 3 | 4 | % Function that will be spawned in our test. Automatically kills itself after 5 | % 5 seconds 6 | test() -> 7 | receive 8 | Msg -> 9 | io:write(Msg) 10 | after 11 | 5000 -> 12 | exit(self(), kill) 13 | end. 14 | 15 | % Spawn a process and add an on_exit handler function that prints out the 16 | % details of the crash 17 | spawn_and_print_crash(Module, Function, Args) -> 18 | Pid = spawn(Module, Function, Args), 19 | StartTime = now(), 20 | Handler = fun(Why) -> 21 | TimeDifference = calculate_time_difference(now(), StartTime), 22 | io:format("==================================================~n"), 23 | io:format("Process ~w died in ~w millisconds with reason: ~w~n", [Pid, TimeDifference, Why]), 24 | io:format("==================================================~n") 25 | end, 26 | on_exit(Pid, Handler). 27 | 28 | % The on_exit function that spawns a monitor and calls `Fun` when the process 29 | % dies 30 | on_exit(Pid, Fun) -> 31 | spawn(fun() -> 32 | Ref = monitor(process, Pid), 33 | receive 34 | {'DOWN', Ref, process, Pid, Why} -> 35 | Fun(Why) 36 | end 37 | end). 38 | 39 | % Utility function for calculating how long the process lived 40 | calculate_time_difference(Current, Past) -> 41 | {Mega, Secs, _} = Current, 42 | {PastMega, PastSecs, _} = Past, 43 | (Mega * 1000000 + Secs) - (PastMega * 1000000 + PastSecs). 44 | 45 | % Function to test this behavior 46 | test_spawn_and_print_crash() -> 47 | ?MODULE:spawn_and_print_crash(?MODULE, test, []). 48 | -------------------------------------------------------------------------------- /chapter_13/exercise_3/timed_process.erl: -------------------------------------------------------------------------------- 1 | -module(timed_process). 2 | -export([test/0, my_spawn/4, test_my_spawn/0]). 3 | 4 | test() -> 5 | receive 6 | Msg -> 7 | io:write(Msg) 8 | after 9 | 10000 -> 10 | exit(self(), kill) 11 | end. 12 | 13 | my_spawn(Module, Function, Args, Seconds) -> 14 | Pid = spawn(Module, Function, Args), 15 | kill_after(Pid, Seconds). 16 | 17 | kill_after(Pid, Seconds) -> 18 | StartTime = now(), 19 | 20 | spawn(fun() -> 21 | Ref = monitor(process, Pid), 22 | receive 23 | {'DOWN', Ref, process, Pid, Why} -> 24 | TimeDifference = calculate_time_difference(now(), StartTime), 25 | io:format("==================================================~n"), 26 | io:format("Process ~w died in ~w millisconds with reason: ~w~n", [Pid, TimeDifference, Why]), 27 | io:format("==================================================~n") 28 | after 29 | Seconds -> 30 | io:format("==================================================~n"), 31 | io:format("Process ~w ran too long. Killed after ~w seconds~n", [Pid, Seconds]), 32 | io:format("==================================================~n"), 33 | exit(Pid, kill) 34 | end 35 | end). 36 | 37 | calculate_time_difference(Current, Past) -> 38 | {Mega, Secs, _} = Current, 39 | {PastMega, PastSecs, _} = Past, 40 | (Mega * 1000000 + Secs) - (PastMega * 1000000 + PastSecs). 41 | 42 | test_my_spawn() -> 43 | ?MODULE:my_spawn(?MODULE, test, [], 3). 44 | -------------------------------------------------------------------------------- /chapter_13/exercise_4/monitored_process.erl: -------------------------------------------------------------------------------- 1 | -module(monitored_process). 2 | -export([loop/0, spawn_registered/1, monitor_process/2, test/0]). 3 | 4 | -define(FIVE_SECONDS, 5000). 5 | 6 | % Exported functions 7 | spawn_registered(Name) -> 8 | % Spawn the process 9 | Pid = spawn(monitored_process, loop, []), 10 | % Then register it under the given name 11 | register(Name, Pid), 12 | % Return the pid to the caller 13 | Pid. 14 | 15 | loop() -> 16 | receive 17 | after 18 | ?FIVE_SECONDS -> 19 | % After 5 seconds print a message, then call loop/0 again 20 | io:format("I'm still running~n", []), 21 | loop() 22 | end. 23 | 24 | monitor_process(Pid, RestartFun) -> 25 | on_exit(Pid, fun(_Why) -> 26 | NewPid = RestartFun(), 27 | monitor_process(NewPid, RestartFun) 28 | end). 29 | 30 | % Test function 31 | test() -> 32 | ProcessName = looping_process, 33 | 34 | % Spawn the process 35 | Pid = spawn_registered(ProcessName), 36 | 37 | % Then monitor it 38 | monitor_process(Pid, fun() -> 39 | spawn_registered(ProcessName) 40 | end), 41 | 42 | % Kill the process 43 | exit(Pid, kill), 44 | % Wait for it to die 45 | timer:sleep(500), 46 | 47 | % ...and make sure it is restarted 48 | NewPid = whereis(ProcessName), 49 | true = is_pid(NewPid), 50 | 51 | passed. 52 | 53 | % Private functions 54 | on_exit(Pid, Fun) -> 55 | % Spawn a monitor process 56 | spawn(fun() -> 57 | % Monitor the process we care about 58 | Ref = monitor(process, Pid), 59 | receive 60 | {'DOWN', Ref, process, Pid, Why} -> 61 | % When the process we are monitor dies, invoke the 62 | % callback function 63 | Fun(Why) 64 | end 65 | end). 66 | -------------------------------------------------------------------------------- /chapter_13/spawn_monitored.erl: -------------------------------------------------------------------------------- 1 | -module(spawn_monitored). 2 | -export([test/0, spawn_and_print_crash/3, test_spawn_and_print_crash/0]). 3 | 4 | test() -> 5 | receive 6 | Msg -> 7 | io:write(Msg) 8 | after 9 | 5000 -> 10 | exit(self(), kill) 11 | end. 12 | 13 | spawn_and_print_crash(Module, Function, Args) -> 14 | {Pid, Ref} = spawn_monitor(Module, Function, Args), 15 | StartTime = now(), 16 | 17 | receive 18 | {'DOWN', Ref, process, Pid, Why} -> 19 | TimeDifference = calculate_time_difference(now(), StartTime), 20 | io:format("==================================================~n"), 21 | io:format("Process ~w died in ~w millisconds with reason: ~w~n", [Pid, TimeDifference, Why]), 22 | io:format("==================================================~n") 23 | end. 24 | 25 | calculate_time_difference(Current, Past) -> 26 | {Mega, Secs, _} = Current, 27 | {PastMega, PastSecs, _} = Past, 28 | (Mega * 1000000 + Secs) - (PastMega * 1000000 + PastSecs). 29 | 30 | test_spawn_and_print_crash() -> 31 | ?MODULE:spawn_and_print_crash(?MODULE, test, []). 32 | -------------------------------------------------------------------------------- /chapter_14/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 14 2 | 3 | **1. Start two nodes on the same host. Perform some RPC calls between the two nodes.** 4 | 5 | The manual pages for `rpc` can be viewed here: http://erlang.org/doc/man/rpc.html 6 | 7 | In one shell session: 8 | 9 | ``` 10 | $ erl -sname watson 11 | % After both shells are running 12 | 1> rpc:call('holmes@my-laptop', erlang, nodes, []). 13 | ``` 14 | 15 | In another shell session: 16 | 17 | ``` 18 | $ erl -sname holmes 19 | 1> rpc:call('watson@my-laptop', erlang, get_cookie, []). 20 | % After watson node is stopped 21 | 2> rpc:call('watson@my-laptop', erlang, nodes, []). 22 | {badrpc,nodedown} 23 | ``` 24 | 25 | **2. Repeat the first exercise with two nodes on a LAN.** 26 | 27 | Same as above, but with a common cookie specified on both computers, and node names in the rpc calls containing the full hostname to the server or IP address. 28 | 29 | **3. Repeat the first exercise with two nodes on separate networks.** 30 | 31 | Make sure port 4369 is open on both computers, and then choose a range of ports to open for communication between individuals nodes. Then run the same command as for exercise #2, but specify the range of open ports with the `kernel inet_dist_listen_min inet_dist_listen_max `. 32 | 33 | **4. Write a simple file server using the libraries in lib_chan.** 34 | 35 | Solution is in the `exercise_4/` directory. My implementation is a very minimal file server. Compile the lib_chan and yaf server code: 36 | 37 | ``` 38 | cd exercise_4/ 39 | erlc *.erl 40 | erlc lib_chan/*.erl 41 | ``` 42 | 43 | Then start the server in an `erl` shell: 44 | 45 | ``` 46 | erl -pa lib_chan -pa . 47 | 1> yaf_server:start(). 48 | lib_chan starting:"lib_chan.conf" 49 | ConfigData=[{port,1234}, 50 | {service,yaf_server,password,"123files",mfa,yaf_server, 51 | start_file_server_loop, 52 | [{location,"shared_files"}]}] 53 | ``` 54 | 55 | In another shell make the client calls: 56 | 57 | ``` 58 | erl -pa lib_chan -pa . 59 | 1> {ok, Pid} = lib_chan:connect("localhost", 1234, "123files", yaf_server, []). 60 | % List files 61 | 2> lib_chan:rpc(Pid, {ls, "/"}). 62 | {ok,["afile.txt"]} 63 | % Read a file 64 | 3> lib_chan:rpc(Pid, {read, "/afile.txt"}). 65 | {ok,<<"You read a file from the server!\n">>} 66 | % Write a new file 67 | 4>lib_chan:rpc(Pid, {write, "another_file.txt", "New file!!!"}). 68 | ok 69 | ``` 70 | -------------------------------------------------------------------------------- /chapter_14/exercise_4/lib_chan.conf: -------------------------------------------------------------------------------- 1 | {port, 1234}. 2 | {service, yaf_server, password, "123files", mfa, yaf_server, start_file_server_loop, [{directory, "shared_files"}]}. 3 | -------------------------------------------------------------------------------- /chapter_14/exercise_4/lib_chan/lib_chan_auth.erl: -------------------------------------------------------------------------------- 1 | -module(lib_chan_auth). 2 | 3 | -export([make_challenge/0, make_response/2, is_response_correct/3]). 4 | 5 | make_challenge() -> 6 | random_string(25). 7 | 8 | make_response(Challenge, Secret) -> 9 | lib_md5:string(Challenge ++ Secret). 10 | 11 | is_response_correct(Challenge, Response, Secret) -> 12 | case lib_md5:string(Challenge ++ Secret) of 13 | Response -> true; 14 | _ -> false 15 | end. 16 | 17 | %% random_string(N) -> a random string with N characters. 18 | 19 | random_string(N) -> random_seed(), random_string(N, []). 20 | 21 | random_string(0, D) -> D; 22 | random_string(N, D) -> 23 | random_string(N-1, [random:uniform(26)-1+$a|D]). 24 | 25 | random_seed() -> 26 | {_,_,X} = erlang:now(), 27 | {H,M,S} = time(), 28 | H1 = H * X rem 32767, 29 | M1 = M * X rem 32767, 30 | S1 = S * X rem 32767, 31 | put(random_seed, {H1,M1,S1}). 32 | -------------------------------------------------------------------------------- /chapter_14/exercise_4/lib_chan/lib_chan_mm.erl: -------------------------------------------------------------------------------- 1 | %% Protocol 2 | %% To the controlling process 3 | %% {chan, MM, Term} 4 | %% {chan_closed, MM} 5 | %% From any process 6 | %% {send, Term} 7 | %% close 8 | 9 | -module(lib_chan_mm). 10 | %% TCP Middle man 11 | %% Models the interface to gen_tcp 12 | 13 | -export([loop/2, send/2, close/1, controller/2, set_trace/2, trace_with_tag/2]). 14 | 15 | send(Pid, Term) -> Pid ! {send, Term}. 16 | close(Pid) -> Pid ! close. 17 | controller(Pid, Pid1) -> Pid ! {setController, Pid1}. 18 | set_trace(Pid, X) -> Pid ! {trace, X}. 19 | 20 | trace_with_tag(Pid, Tag) -> 21 | set_trace(Pid, {true, 22 | fun(Msg) -> 23 | io:format("MM:~p ~p~n",[Tag, Msg]) 24 | end}). 25 | 26 | loop(Socket, Pid) -> 27 | %% trace_with_tag(self(), trace), 28 | process_flag(trap_exit, true), 29 | loop1(Socket, Pid, false). 30 | 31 | loop1(Socket, Pid, Trace) -> 32 | receive 33 | {tcp, Socket, Bin} -> 34 | Term = binary_to_term(Bin), 35 | trace_it(Trace,{socketReceived, Term}), 36 | Pid ! {chan, self(), Term}, 37 | loop1(Socket, Pid, Trace); 38 | {tcp_closed, Socket} -> 39 | trace_it(Trace, socketClosed), 40 | Pid ! {chan_closed, self()}; 41 | {'EXIT', Pid, Why} -> 42 | trace_it(Trace,{controllingProcessExit, Why}), 43 | gen_tcp:close(Socket); 44 | {setController, Pid1} -> 45 | trace_it(Trace, {changedController, Pid}), 46 | loop1(Socket, Pid1, Trace); 47 | {trace, Trace1} -> 48 | trace_it(Trace, {setTrace, Trace1}), 49 | loop1(Socket, Pid, Trace1); 50 | close -> 51 | trace_it(Trace, closedByClient), 52 | gen_tcp:close(Socket); 53 | {send, Term} -> 54 | trace_it(Trace, {sendingMessage, Term}), 55 | gen_tcp:send(Socket, term_to_binary(Term)), 56 | loop1(Socket, Pid, Trace); 57 | UUg -> 58 | io:format("lib_chan_mm: protocol error:~p~n",[UUg]), 59 | loop1(Socket, Pid, Trace) 60 | end. 61 | trace_it(false, _) -> void; 62 | trace_it({true, F}, M) -> F(M). 63 | -------------------------------------------------------------------------------- /chapter_14/exercise_4/lib_chan/lib_chan_test.erl: -------------------------------------------------------------------------------- 1 | -module(lib_chan_test). 2 | 3 | -compile(export_all). 4 | 5 | -import(lib_chan, [connect/5, rpc/4, disconnect/1]). 6 | 7 | start_server() -> 8 | lib_chan:start_server(), 9 | wait(). 10 | 11 | start_client() -> 12 | io:format("Running client tests~n"), 13 | tests(1). 14 | 15 | test(1) -> 16 | {ok, MM} = connect("localhost", 1234, echo, "echo1", {echo,client,arg1}), 17 | MM ! {send, "hello"}, 18 | receive 19 | {chan, MM, "hello"} -> 20 | io:format("test(1) succeeded~n"); 21 | Other -> 22 | io:format("test(1) failed:~p~n", [Other]) 23 | end, 24 | exit(MM, close); 25 | test(2) -> 26 | {ok, MM} = connect("localhost", 1234, math, "qwerty", ""), 27 | MM ! {send, {factorial, 6}}, 28 | receive 29 | {chan, MM, 720} -> 30 | io:format("test(2) succeeded~n"); 31 | Other -> 32 | io:format("test(2) failed:~p~n", [Other]) 33 | end, 34 | MM ! close; 35 | test(3) -> 36 | case connect("localhost", 1234, mod_non_exists, "facSecret", "") of 37 | {error, badService} -> 38 | io:format("test(3) succeeded~n"); 39 | Other -> 40 | io:format("test(3) failed:~p~n", [Other]) 41 | end; 42 | test(4) -> 43 | case connect("localhost", 1234, math, "badSecret", "") of 44 | {error, authFail} -> 45 | io:format("test(4) succeeded~n"); 46 | Other -> 47 | io:format("test(4) failed:~p~n",[Other]) 48 | end; 49 | test(5) -> 50 | {ok, MM} = connect("localhost", 1234, srpc, "secret", ""), 51 | case mod_srpc:rpc(MM, test1, fac, [20]) of 52 | 2432902008176640000 -> 53 | io:format("test(5) succeeded~n"); 54 | Other -> 55 | io:format("test(5) failed:~p~n",[Other]) 56 | end, 57 | disconnect(MM); 58 | test(6) -> 59 | {ok, MM} = connect("localhost", 1234, srpc, "secret", ""), 60 | case (catch mod_srpc:rpc(MM, test2, fac, [20])) of 61 | {'EXIT', {modNotAllowed, test2}} -> 62 | io:format("test(6) succeeded~n"); 63 | Other -> 64 | io:format("test(6) failed:~p~n",[Other]) 65 | end, 66 | disconnect(MM); 67 | test(7) -> 68 | {ok, MM} = connect("localhost", 1234, root, "verySecret", ""), 69 | case (catch mod_srpc:rpc(MM, any_apply, mfa, [test1, fac, [20]])) of 70 | 2432902008176640000 -> 71 | io:format("test(7) succeeded~n"); 72 | Other -> 73 | io:format("test(7) failed:~p~n",[Other]) 74 | end; 75 | test(8) -> 76 | done. 77 | 78 | fac(0) -> 1; 79 | fac(N) -> N * fac(N-1). 80 | 81 | tests(N) -> 82 | case test(N) of 83 | done -> init:stop(); 84 | _Val -> tests(N+1) 85 | end. 86 | 87 | wait() -> 88 | receive 89 | _Any -> 90 | wait() 91 | end. 92 | 93 | -------------------------------------------------------------------------------- /chapter_14/exercise_4/lib_chan/lib_md5.erl: -------------------------------------------------------------------------------- 1 | -module(lib_md5). 2 | 3 | %% modfied to use BIFs 4 | 5 | -export([string/1, file/1, bin/1, binAsBin/1, digest2str/1]). 6 | 7 | -define(BLOCKSIZE, 32768). 8 | -define(IN(X,Min,Max), X >= Min, X =< Max). 9 | 10 | %% md5:string(string()) -> BinDigest 11 | %% md5:file(FileName) -> {ok, BinDigest} | {error, E} 12 | %% md5:digest2str(BinDigest) -> StringDigest 13 | 14 | %% md5:file works with chunks so should work correctly with extremely 15 | %% large files 16 | 17 | string(Str) -> digest2str(erlang:md5(Str)). 18 | 19 | file(File) -> 20 | case file:open(File, [binary,raw,read]) of 21 | {ok, P} -> loop(P, erlang:md5_init()); 22 | Error -> Error 23 | end. 24 | 25 | loop(P, C) -> 26 | case file:read(P, ?BLOCKSIZE) of 27 | {ok, Bin} -> 28 | loop(P, erlang:md5_update(C, Bin)); 29 | eof -> 30 | file:close(P), 31 | {ok, erlang:md5_final(C)} 32 | end. 33 | 34 | digest2str(Digest) -> bin2str(binary_to_list(Digest)). 35 | 36 | bin2str([H|T]) -> 37 | {H1, H2} = byte2hex(H), 38 | [H1,H2|bin2str(T)]; 39 | bin2str([]) -> 40 | []. 41 | 42 | byte2hex(X) -> 43 | {nibble2hex(X bsr 4), nibble2hex(X band 15)}. 44 | 45 | nibble2hex(X) when ?IN(X, 0, 9) -> X + $0; 46 | nibble2hex(X) when ?IN(X, 10, 15) -> X - 10 + $a. 47 | 48 | %% compute the md5 checksum of a binary 49 | bin(Bin) -> 50 | C1 = erlang:md5_init(), 51 | C2 = erlang:md5_update(C1, Bin), 52 | C3 = erlang:md5_final(C2), 53 | digest2str(C3). 54 | 55 | binAsBin(Bin) -> 56 | C1 = erlang:md5_init(), 57 | C2 = erlang:md5_update(C1, Bin), 58 | erlang:md5_final(C2). 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /chapter_14/exercise_4/shared_files/afile.txt: -------------------------------------------------------------------------------- 1 | You read a file from the server! 2 | -------------------------------------------------------------------------------- /chapter_14/exercise_4/yaf_server.erl: -------------------------------------------------------------------------------- 1 | -module(yaf_server). 2 | 3 | -export([start/0, start_file_server_loop/3, loop/2]). 4 | 5 | start() -> 6 | lib_chan:start_server("lib_chan.conf"). 7 | 8 | start_file_server_loop(MM, _ArgsC, ArgsServer) -> 9 | {_, Directory} = lists:keyfind(directory, 1, ArgsServer), 10 | 11 | loop(MM, Directory). 12 | 13 | loop(MM, Directory) -> 14 | receive 15 | {chan, MM, {read, Path}} -> 16 | % Read file at Path and send the contents to the client 17 | MM ! {send, file:read_file(path(Directory, Path))}, 18 | loop(MM, Directory); 19 | 20 | {chan, MM, {write, Path, Contents}} -> 21 | % Write Contents to file at Path 22 | MM ! {send, file:write_file(path(Directory, Path), Contents)}, 23 | loop(MM, Directory); 24 | 25 | {chan, MM, {ls, Path}} -> 26 | % List files in directory at Path 27 | MM ! {send, file:list_dir(path(Directory, Path))}, 28 | loop(MM, Directory); 29 | 30 | {chan_closed, MM} -> 31 | loop(MM, Directory) 32 | end. 33 | 34 | path(Directory, Path) -> 35 | filename:flatten([Directory, "/", Path]). 36 | -------------------------------------------------------------------------------- /chapter_15/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 15 2 | 3 | **1. Download the code for the port driver given earlier and test it.** 4 | 5 | Nothing needed for this exercise. 6 | 7 | **2. Download code for a linked in driver and test it on your system** 8 | 9 | The book says to download drivers from git://github.com/erlang/linked_in_drivers.git but this repository no longer exists. 10 | 11 | **3. Find an operating system command to discover what kind of CPU your computer has. Write a function that returns your CPU type using the `os:cmd/1`** 12 | 13 | Solution in the `exercise_3/` directory. 14 | 15 | ``` 16 | erlc cpu_info.erl 17 | erl 18 | 1> cpu_info:get(). 19 | [{"Architecture","x86_64"}, 20 | {"CPU op-mode","32-bit, 64-bit"}, 21 | {"Model name","Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz"}, 22 | ... 23 | 2> cpu_info:get("Model name"). 24 | "Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz" 25 | ``` 26 | -------------------------------------------------------------------------------- /chapter_15/exercise_3/cpu_info.erl: -------------------------------------------------------------------------------- 1 | -module(cpu_info). 2 | 3 | -export([get/0, get/1]). 4 | 5 | % Get all information about the CPU 6 | get() -> 7 | cpu_info(). 8 | 9 | % Get a specific value from the CPU information 10 | % 11 | % Example: 12 | % 13 | % 1> cpu_info:get("Model name"). 14 | % {"Model name","Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz"} 15 | get(Key) -> 16 | Proplist = cpu_info(), 17 | proplists:get_value(Key, Proplist). 18 | 19 | cpu_info() -> 20 | % Get the raw command output 21 | Output = os:cmd("lscpu"), 22 | % Split output into lines 23 | Lines = re:split(Output, "\n"), 24 | % Convert to key value tuples 25 | to_key_value_tuples(Lines). 26 | 27 | to_key_value_tuples([]) -> []; 28 | to_key_value_tuples([<<>>]) -> []; 29 | to_key_value_tuples([Line|Lines]) -> 30 | [RawKey, RawValue] = re:split(Line, ":"), 31 | % Cleanup keys and values 32 | Value = trim(RawValue), 33 | Key = cleanup_key(RawKey), 34 | [{Key, Value}|to_key_value_tuples(Lines)]. 35 | 36 | cleanup_key(Key) -> 37 | remove_parens(trim(Key)). 38 | 39 | remove_parens(String) -> 40 | re:replace(String, "\\([^)]*\\)", "", [global,{return,list}]). 41 | 42 | trim(String) -> 43 | re:replace(String, "(^\\s+)|(\\s+$)", "", [global,{return,list}]). 44 | -------------------------------------------------------------------------------- /chapter_16/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 16 2 | 3 | **1. Write a program that compares the last modified timestamp of a .erl file and its .beam counterpart** 4 | 5 | Solution in the `exercise_1/` directory. I put the code in an escript so it is easy to use on the command line. 6 | 7 | ```bash 8 | $ ./stale_beam ../../chapter_15/exercise_3/cpu_info.erl 9 | ``` 10 | 11 | **2. Use erlang:md5/1 to compute the checksum of a small file** 12 | 13 | Solution in the `exercise_2/` directory. I put the code in an escript so it is easy to use on the command line. 14 | 15 | ```bash 16 | $ ./emd5sum emd5sum 17 | 07357C74F0992326718D133D7DDD5F01 18 | ``` 19 | 20 | **3. Repeat the previous exercise for a large file (a few hundred megabytes). Read the file in chunks and use `erlang:md5_init`, `erlang:md5_update` and `erlang:md5_final` to compute the sum.** 21 | 22 | Solution in the `exercise_3/` directory. I put the code in an escript so it is easy to use on the command line. 23 | 24 | ```bash 25 | # Generate a large file 26 | $ fallocate -l 256mb exercise_3/bigfile.bin 27 | # Compute md5 sum 28 | $ ./emd5sum emd5sum 29 | 07357C74F0992326718D133D7DDD5F01 30 | ``` 31 | 32 | **4. Use the `lib_find` module to find all `.jpg` files on your computer. Check for identical files by computing the MD5 sum of each file and comparing the computed sums.** 33 | 34 | Solution in the `exercise_5/` directory. 35 | 36 | ```bash 37 | # Run the escript and it will use lib_find to find all duplicate JPEGs. Duplicate JPEGs will be printed. 38 | $ ./find_duplicate_jpegs 39 | ``` 40 | 41 | **5. Write a caching mechanism that computes the MD5 sum of a file and stores it with the last modified time of the file. When the sum is requested check if the file has changed and return the cached sum if it hasn't** 42 | 43 | Solution in the `exercise_5/` directory. 44 | 45 | ``` 46 | $ erlc md5_cache.erl 47 | $ erl 48 | Erlang/OTP 19 [erts-8.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] 49 | Eshell V8.2 (abort with ^G) 50 | 1> md5_cache:start_link(). 51 | {ok,<0.66.0>} 52 | 2> md5_cache:get_md5("md5_cache.erl"). 53 | MD5 hash is not up to date 54 | {ok,<<230,12,220,207,212,102,157,69,193,161,120,23,149, 55 | 187,231,52>>} 56 | 3> md5_cache:get_md5("md5_cache.erl"). 57 | MD5 hash is up to date 58 | {ok,<<230,12,220,207,212,102,157,69,193,161,120,23,149, 59 | 187,231,52>>} 60 | ``` 61 | 62 | **6. Tweets are exactly 140 bytes long. Write a random access twit storage module that exports the following functions: `init(N)` allocates space for N number of tweets. `store(N, Buf)` to store tweet (140 byte Buf) at N location. `fetch(N)` fetches the data for tweet number N.** 63 | 64 | Solution in the `exercise_6/` directory. 65 | ``` 66 | erl 67 | 1> tweet_store:init(3). 68 | ok 69 | 2> tweet_store:store(2, <<"A short tweet">>). 70 | ok 71 | 3> tweet_store:store(3, <<"Another short tweet">>). 72 | ok 73 | 4> tweet_store:fetch(2). 74 | <<"A short tweet">> 75 | 5> tweet_store:fetch(3). 76 | <<"Another short tweet">> 77 | 6> tweet_store:fetch(1). 78 | <<>> 79 | 7> 80 | ``` 81 | -------------------------------------------------------------------------------- /chapter_16/exercise_1/stale_beam: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | 3 | -include_lib("kernel/include/file.hrl"). 4 | 5 | print_usage() -> 6 | io:format("stale_beam some/erlang/module.erl"). 7 | 8 | main([Filename]) -> 9 | {ok, #file_info{mtime=ModifiedTime}} = file:read_file_info(Filename), 10 | BeamFilename = beam_filename(Filename), 11 | {ok, #file_info{mtime=BeamModifiedTime}} = file:read_file_info(BeamFilename), 12 | %io:format("Modified Time: ~p~n", [ModifiedTime]), 13 | %io:format("Beam Modified Time: ~p~n", [BeamModifiedTime]), 14 | 15 | {Days, _Time} = calendar:time_difference(ModifiedTime, BeamModifiedTime), 16 | case Days of 17 | Days when Days >= 0 -> 18 | io:format("Beam file is up to date~n", []); 19 | _ -> 20 | io:format("Beam file is out of date. Recompile~n", []) 21 | end; 22 | 23 | main(_) -> 24 | % Print usage message if invalid number of arguments is passed in 25 | print_usage(), 26 | halt(1). 27 | 28 | % Use regex to replace .erl with .beam in the filenames 29 | beam_filename(FilenameErl) -> 30 | re:replace(FilenameErl, "\.erl$", ".beam", [{return, list}]). 31 | -------------------------------------------------------------------------------- /chapter_16/exercise_2/emd5sum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | 3 | -include_lib("kernel/include/file.hrl"). 4 | 5 | print_usage() -> 6 | io:format("./emd5sum ~n"). 7 | 8 | main([Filename]) -> 9 | {ok, Data} = file:read_file(Filename), 10 | Sum = erlang:md5(Data), 11 | io:format("~s~n", [hex_string(Sum)]), 12 | halt(0); 13 | main(_) -> 14 | % Print usage message if invalid number of arguments is passed in 15 | print_usage(), 16 | halt(1). 17 | 18 | % From https://stackoverflow.com/questions/3768197/erlang-ioformatting-a-binary-to-hex 19 | % We need to format each byte in the hex binary as a set of two ascii 20 | % characters in order to construct an printable string of characters. 21 | hex_string(HexBin) -> 22 | [io_lib:format("~2.16.0B",[X]) || <> <= HexBin]. 23 | -------------------------------------------------------------------------------- /chapter_16/exercise_3/emd5sum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | 3 | -include_lib("kernel/include/file.hrl"). 4 | 5 | print_usage() -> 6 | io:format("./emd5sum ~n"). 7 | 8 | main([Filename]) -> 9 | {ok, File} = file:open(Filename, [read, binary, raw]), 10 | Sum = compute_md5_sum(File), 11 | file:close(File), 12 | io:format("~s~n", [hex_string(Sum)]), 13 | halt(0); 14 | main(_) -> 15 | % Print usage message if invalid number of arguments is passed in 16 | print_usage(), 17 | halt(1). 18 | 19 | compute_md5_sum(File) -> 20 | % Compute md5 sum incrementally 21 | Context = erlang:md5_init(), 22 | NewContext = compute_md5_sum(File, Context), 23 | % Return the file md5 sum 24 | erlang:md5_final(NewContext). 25 | 26 | compute_md5_sum(File, Context) -> 27 | % Read more of the file 28 | case file:read(File, 4096) of 29 | {ok, Data} -> 30 | % Update context with new data 31 | NewContext = erlang:md5_update(Context, Data), 32 | compute_md5_sum(File, NewContext); 33 | eof -> 34 | Context 35 | end. 36 | 37 | % From https://stackoverflow.com/questions/3768197/erlang-ioformatting-a-binary-to-hex 38 | % We need to format each byte in the hex binary as a set of two ascii 39 | % characters in order to construct an printable string of characters. 40 | hex_string(HexBin) -> 41 | [io_lib:format("~2.16.0B",[X]) || <> <= HexBin]. 42 | -------------------------------------------------------------------------------- /chapter_16/exercise_5/md5_cache.erl: -------------------------------------------------------------------------------- 1 | -module(md5_cache). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start_link/0, get_md5/1]). 6 | 7 | -include_lib("kernel/include/file.hrl"). 8 | 9 | -define(SERVER, ?MODULE). 10 | 11 | %% gen_server callbacks 12 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 13 | terminate/2, code_change/3]). 14 | 15 | start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). 16 | 17 | get_md5(Filename) -> 18 | gen_server:call(?SERVER, {get_md5, Filename}). 19 | 20 | % Callbacks 21 | init([]) -> {ok, []}. 22 | 23 | handle_call({get_md5, Filename}, _From, State) -> 24 | case proplists:lookup(Filename, State) of 25 | none -> 26 | % Compute MD5 hash 27 | {ok, Data} = file:read_file(Filename), 28 | Sum = erlang:md5(Data), 29 | % Get the last modified date of the file 30 | {ok, #file_info{mtime=ModifiedTime}} = file:read_file_info(Filename), 31 | io:format("MD5 hash is not up to date~n", []), 32 | 33 | % Update the server state with the cached MD5 sum and return the sum 34 | % to the caller 35 | {reply, {ok, Sum}, [{Filename, ModifiedTime, Sum}|State]}; 36 | {Filename, Time, Md5} -> 37 | % Check if the last modified date has changed 38 | {ok, #file_info{mtime=ModifiedTime}} = file:read_file_info(Filename), 39 | {Days, _Time} = calendar:time_difference(ModifiedTime, Time), 40 | 41 | Sum = case Days of 42 | Days when Days =/= 0 -> 43 | % If it has changed update the hash 44 | io:format("MD5 hash is not up to date~n", []), 45 | {ok, Data} = file:read_file(Filename), 46 | erlang:md5(Data); 47 | _ -> 48 | % Otherwise return the cached MD5 hash 49 | io:format("MD5 hash is up to date~n", []), 50 | Md5 51 | end, 52 | 53 | NewState = lists:keyreplace(Filename, 1, State, {Filename, ModifiedTime, Sum}), 54 | {reply, {ok, Md5}, NewState} 55 | end. 56 | 57 | handle_cast(_Msg, State) -> {noreply, State}. 58 | handle_info(_Info, State) -> {noreply, State}. 59 | terminate(_Reason, _State) -> ok. 60 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 61 | -------------------------------------------------------------------------------- /chapter_16/exercise_6/tweet_store.erl: -------------------------------------------------------------------------------- 1 | -module(tweet_store). 2 | 3 | -export([init/1, store/2, fetch/1]). 4 | 5 | -define(FILENAME, "tweet_store.data"). 6 | -define(TWEET_SIZE, 140). 7 | 8 | init(K) -> 9 | BufferSize = K * ?TWEET_SIZE, 10 | Store = open_store(), 11 | % Allocate a file of the correct size 12 | ok = file:allocate(Store, 0, BufferSize), 13 | close_store(Store). 14 | 15 | store(N, Tweet) when is_list(Tweet), length(Tweet) =< ?TWEET_SIZE -> 16 | store(N, list_to_binary(Tweet)); 17 | store(N, Tweet) when is_binary(Tweet), byte_size(Tweet) =< ?TWEET_SIZE -> 18 | Store = open_store(), 19 | % Write tweet to file at the right location 20 | ok = file:pwrite(Store, (N - 1) * ?TWEET_SIZE, Tweet), 21 | close_store(Store). 22 | 23 | fetch(N) -> 24 | Store = open_store(), 25 | % Read 140 bytes from the location for tweet N 26 | case file:pread(Store, (N - 1) * ?TWEET_SIZE, ?TWEET_SIZE) of 27 | {ok, Tweet} -> 28 | ok = close_store(Store), 29 | trim_trailing_bytes(Tweet); 30 | eof -> 31 | ok = close_store(Store), 32 | eof 33 | end. 34 | 35 | % Private helper functions 36 | open_store() -> 37 | {ok, File} = file:open(?FILENAME, [read, write, binary, raw]), 38 | File. 39 | 40 | close_store(File) -> 41 | file:close(File). 42 | 43 | % From https://stackoverflow.com/questions/5699739/how-do-i-get-binary-byte-length-in-erlang 44 | trim_trailing_bytes(List) when is_list(List) -> 45 | list_to_binary(lists:reverse(lists:dropwhile(fun(0) -> true; (_) -> false end, lists:reverse(List)))); 46 | trim_trailing_bytes(Binary) -> 47 | trim_trailing_bytes(binary_to_list(Binary)). 48 | -------------------------------------------------------------------------------- /chapter_17/exercise_1/nano_get_url.erl: -------------------------------------------------------------------------------- 1 | -module(nano_get_url). 2 | 3 | -export([get_url/0, get_url/1, get_domain/0, get_domain/1]). 4 | 5 | get_url() -> 6 | get_url("http://www.google.com/search?q=erlang"). 7 | 8 | get_url(Url) -> 9 | {_Protocol, Host, Port, Path} = parse_url(Url), 10 | {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {packet, 0}]), 11 | ok = gen_tcp:send(Socket, io_lib:format("GET ~s HTTP/1.0\r\n\r\n", [Path])), 12 | {Status, Headers, Body} = parse_response(receive_data(Socket, [])), 13 | case Status of 14 | "302" -> 15 | % Check if redirect header is present 16 | case proplists:lookup("location", Headers) of 17 | {_, Location} -> 18 | % Follow redirect to location 19 | io:format("Redirected to: ~s~n", [Location]), 20 | get_url(Location); 21 | none -> 22 | io:format("No location header found!~n", []), 23 | {Status, Headers, Body} 24 | end; 25 | _ -> 26 | {Status, Headers, Body} 27 | end. 28 | 29 | get_domain() -> 30 | get_url("www.google.com"). 31 | 32 | get_domain(Host) -> 33 | {ok, Socket} = gen_tcp:connect(Host, 80, [binary, {packet, 0}]), 34 | ok = gen_tcp:send(Socket, "GET / HTTP/1.0\r\n\r\n"), 35 | {Status, Headers, Body} = parse_response(receive_data(Socket, [])), 36 | {Status, Headers, Body}. 37 | 38 | receive_data(Socket, SoFar) -> 39 | receive 40 | {tcp, Socket, Bin} -> 41 | receive_data(Socket, [Bin|SoFar]); 42 | {tcp_closed, Socket} -> 43 | list_to_binary(lists:reverse(SoFar)) 44 | end. 45 | 46 | parse_url(Url) -> 47 | {ok, Parsed} = http_uri:parse(Url), 48 | % We ignore the query string for simplicity here 49 | {Protocol, _, Host, Port, Path, _Query} = Parsed, 50 | {Protocol, Host, Port, Path}. 51 | 52 | parse_response(ResponseBin) -> 53 | % Find end of headers 54 | [Headers, Body] = binary:split(ResponseBin, <<"\r\n\r\n">>), 55 | 56 | [StatusLine|HeaderList] = binary:split(Headers, <<"\r\n">>, [global]), 57 | 58 | % Parse status 59 | {match, [[Status]]} = re:run(StatusLine,"(\\d{3})", [global, {capture, first, list}]), 60 | 61 | % Parse headers 62 | ParsedHeaders = lists:map(fun(Header) -> 63 | [HeaderName, Value] = binary:split(Header, <<":">>), 64 | {lowercase(trim(HeaderName)), trim(Value)} 65 | end, HeaderList), 66 | 67 | 68 | % Return body and parsed headers 69 | {Status, ParsedHeaders, Body}. 70 | 71 | lowercase(String) -> 72 | string:to_lower(String). 73 | 74 | trim(String) -> 75 | re:replace(String, "(^\\s+)|(\\s+$)", "", [global,{return,list}]). 76 | -------------------------------------------------------------------------------- /chapter_17/exercise_2/nano_client.erl: -------------------------------------------------------------------------------- 1 | -module(nano_client). 2 | 3 | -export([send/3]). 4 | 5 | send(Mod, Func, Args) -> 6 | {ok, Socket} = gen_tcp:connect("localhost", 2345, [binary, {packet, 4}]), 7 | ok = gen_tcp:send(Socket, term_to_binary({Mod, Func, Args})), 8 | receive 9 | {tcp, Socket, Bin} -> 10 | io:format("Client received binary = ~p~n", [Bin]), 11 | Val = binary_to_term(Bin), 12 | io:format("Client result = ~p~n", [Val]), 13 | gen_tcp:close(Socket) 14 | end. 15 | -------------------------------------------------------------------------------- /chapter_17/exercise_2/nano_server.erl: -------------------------------------------------------------------------------- 1 | -module(nano_server). 2 | 3 | -export([start/0, loop/1]). 4 | 5 | start() -> 6 | {ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, 7 | {reuseaddr, true}, 8 | {active, true}]), 9 | {ok, Socket} = gen_tcp:accept(Listen), 10 | gen_tcp:close(Listen), 11 | loop(Socket). 12 | 13 | loop(Socket) -> 14 | receive 15 | {tcp, Socket, Bin} -> 16 | io:format("Server received binary = ~p~n", [Bin]), 17 | MFA = {Mod, Func, Args} = binary_to_term(Bin), 18 | io:format("Server (unpacking) ~p~n", [MFA]), 19 | Reply = apply(Mod, Func, Args), 20 | io:format("Server replying = ~p~n", [Reply]), 21 | gen_tcp:send(Socket, term_to_binary(Reply)), 22 | loop(Socket); 23 | {tcp_closed, Socket} -> 24 | io:format("Server socket closed~n") 25 | end. 26 | -------------------------------------------------------------------------------- /chapter_17/exercise_3/nano_client.erl: -------------------------------------------------------------------------------- 1 | -module(nano_client). 2 | 3 | -export([send/3]). 4 | 5 | send(Mod, Func, Args) -> 6 | {ok, Socket} = gen_udp:open(0, [binary]), 7 | ok = gen_udp:send(Socket, "localhost", 2345, term_to_binary({Mod, Func, Args})), 8 | Value = receive 9 | {udp, Socket, _, _, Bin} -> 10 | io:format("Client received binary = ~p~n", [Bin]), 11 | Val = binary_to_term(Bin), 12 | io:format("Client result = ~p~n", [Val]), 13 | Val 14 | after 2000 -> 15 | {error, timeout} 16 | end, 17 | gen_udp:close(Socket), 18 | Value. 19 | -------------------------------------------------------------------------------- /chapter_17/exercise_3/nano_server.erl: -------------------------------------------------------------------------------- 1 | -module(nano_server). 2 | 3 | -export([start/0, loop/1]). 4 | 5 | start() -> 6 | {ok, Socket} = gen_udp:open(2345, [binary]), 7 | loop(Socket). 8 | 9 | loop(Socket) -> 10 | receive 11 | {udp, Socket, Host, Port, Bin} -> 12 | io:format("Server received binary = ~p~n", [Bin]), 13 | MFA = {Mod, Func, Args} = binary_to_term(Bin), 14 | io:format("Server (unpacking) ~p~n", [MFA]), 15 | Reply = apply(Mod, Func, Args), 16 | io:format("Server replying = ~p~n", [Reply]), 17 | gen_udp:send(Socket, Host, Port, term_to_binary(Reply)), 18 | loop(Socket) 19 | end. 20 | -------------------------------------------------------------------------------- /chapter_17/exercise_4/encrypt.erl: -------------------------------------------------------------------------------- 1 | -module(encrypt). 2 | 3 | -export([encrypt/1, decrypt/1]). 4 | 5 | -define(SECRET_KEY_FILE, "secret.key"). 6 | -define(MODE, aes_gcm). 7 | -define(AAD, <<"AES256GCM">>). 8 | -define(IV, <<"ab">>). 9 | 10 | encrypt(Msg) when is_binary(Msg) -> 11 | {CipherText, CipherTag} = crypto:block_encrypt(?MODE, get_secret_key(), ?IV, {?AAD, Msg, 16}), 12 | list_to_binary([CipherTag, CipherText]). 13 | 14 | decrypt(<>) -> 15 | crypto:block_decrypt(?MODE, get_secret_key(), ?IV, {?AAD, CipherText, Tag}). 16 | 17 | get_secret_key() -> 18 | % Check if file exists 19 | EncodedKey = case filelib:is_regular(?SECRET_KEY_FILE) of 20 | true -> 21 | {ok, Contents} = file:read_file(?SECRET_KEY_FILE), 22 | Contents; 23 | _ -> 24 | % If not generate key 25 | Contents = generate_secret_key(), 26 | file:write_file(?SECRET_KEY_FILE, Contents), 27 | Contents 28 | end, 29 | % decode 30 | base64:decode(EncodedKey). 31 | 32 | 33 | 34 | generate_secret_key() -> 35 | Key = crypto:strong_rand_bytes(16), 36 | base64:encode(Key). 37 | -------------------------------------------------------------------------------- /chapter_17/exercise_4/nano_client.erl: -------------------------------------------------------------------------------- 1 | -module(nano_client). 2 | 3 | -export([send/3]). 4 | 5 | send(Mod, Func, Args) -> 6 | {ok, Socket} = gen_tcp:connect("localhost", 2345, [binary, {packet, 4}]), 7 | ok = gen_tcp:send(Socket, encrypt:encrypt(term_to_binary({Mod, Func, Args}))), 8 | receive 9 | {tcp, Socket, EncryptedBin} -> 10 | Bin = encrypt:decrypt(EncryptedBin), 11 | io:format("Client received binary = ~p~n", [Bin]), 12 | Val = binary_to_term(Bin), 13 | io:format("Client result = ~p~n", [Val]), 14 | gen_tcp:close(Socket) 15 | end. 16 | -------------------------------------------------------------------------------- /chapter_17/exercise_4/nano_server.erl: -------------------------------------------------------------------------------- 1 | -module(nano_server). 2 | 3 | -export([start/0, loop/1]). 4 | 5 | start() -> 6 | {ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, 7 | {reuseaddr, true}, 8 | {active, true}]), 9 | {ok, Socket} = gen_tcp:accept(Listen), 10 | gen_tcp:close(Listen), 11 | loop(Socket). 12 | 13 | loop(Socket) -> 14 | receive 15 | {tcp, Socket, EncryptedBin} -> 16 | Bin = encrypt:decrypt(EncryptedBin), 17 | io:format("Server received binary = ~p~n", [Bin]), 18 | MFA = {Mod, Func, Args} = binary_to_term(Bin), 19 | io:format("Server (unpacking) ~p~n", [MFA]), 20 | Reply = apply(Mod, Func, Args), 21 | io:format("Server replying = ~p~n", [Reply]), 22 | gen_tcp:send(Socket, encrypt:encrypt(term_to_binary(Reply))), 23 | loop(Socket); 24 | {tcp_closed, Socket} -> 25 | io:format("Server socket closed~n") 26 | end. 27 | -------------------------------------------------------------------------------- /chapter_17/exercise_5/nano_client.erl: -------------------------------------------------------------------------------- 1 | -module(nano_client). 2 | 3 | -export([send_msg/3, receive_msgs/2, send/3]). 4 | 5 | % Define addresses as username - hostname tuples instead of an email address 6 | % {username, hostname} 7 | 8 | % Simple function for sending messages to a recipient identified by 9 | % username/hostname combination. 10 | send_msg(Username, Hostname, Msg) -> 11 | send(Username, Hostname, {send_msg, Msg}). 12 | 13 | % Receive all messages sent to a specific address 14 | receive_msgs(Username, Hostname) -> 15 | send(Username, Hostname, get_all_msgs). 16 | 17 | send(Username, Hostname, Msg) -> 18 | {ok, Socket} = gen_tcp:connect(Hostname, 2345, [binary, {packet, 4}, {port, 0}]), 19 | ok = gen_tcp:send(Socket, term_to_binary({Username, Msg})), 20 | receive 21 | {tcp, Socket, Bin} -> 22 | io:format("Client received binary = ~p~n", [Bin]), 23 | Val = binary_to_term(Bin), 24 | io:format("Client result = ~p~n", [Val]), 25 | gen_tcp:close(Socket) 26 | end. 27 | -------------------------------------------------------------------------------- /chapter_17/exercise_5/nano_server.erl: -------------------------------------------------------------------------------- 1 | -module(nano_server). 2 | 3 | -export([start/0, loop/1]). 4 | 5 | start() -> 6 | {ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, 7 | {keepalive, true}, 8 | {reuseaddr, true}, 9 | {active, true}]), 10 | {ok, Socket} = gen_tcp:accept(Listen), 11 | gen_tcp:close(Listen), 12 | loop(Socket). 13 | 14 | loop(Socket) -> 15 | receive 16 | {tcp, Socket, Bin} -> 17 | io:format("Server received binary = ~p~n", [Bin]), 18 | {Username, Msg} = Raw = binary_to_term(Bin), 19 | io:format("Server (unpacking) ~p~n", [Raw]), 20 | Reply = handle_message(Username, Msg), 21 | io:format("Server replying = ~p~n", [Reply]), 22 | gen_tcp:send(Socket, term_to_binary(Reply)), 23 | loop(Socket); 24 | {tcp_closed, Socket} -> 25 | io:format("Server socket closed~n"), 26 | start() 27 | end. 28 | 29 | handle_message(Username, Msg) -> 30 | % Dispatch messages to the correct function 31 | case Msg of 32 | {send_msg, EmailMessage} -> 33 | send_message(Username, EmailMessage); 34 | get_all_msgs -> 35 | read_messages(Username); 36 | _ -> 37 | unknown_message 38 | end. 39 | 40 | % Read all messages for user off disk 41 | read_messages(Username) -> 42 | Directory = ensure_mbox_directory_exists(Username), 43 | {ok, Filenames} = file:list_dir(Directory), 44 | lists:map(fun(Filename) -> 45 | io:format("Filename: ~p", [Filename]), 46 | {ok, Contents} = file:read_file(filename:join([Directory, Filename])), 47 | Title = filename:basename(Filename), 48 | {Title, Contents} 49 | end, Filenames). 50 | 51 | send_message(Username, Msg) -> 52 | Directory = ensure_mbox_directory_exists(Username), 53 | io:format("Writing ~p~n", [Directory]), 54 | % Write message to disk 55 | Filename = lists:flatten(io_lib:format("~p-message.txt",[erlang:monotonic_time() * -1])), 56 | FullFilename = filename:join([Directory, Filename]), 57 | io:format("Writing ~p~n", [FullFilename]), 58 | ok = file:write_file(FullFilename, Msg). 59 | 60 | ensure_mbox_directory_exists(Username) when is_binary(Username); is_list(Username) -> 61 | MboxDir = mbox_base_directory(), 62 | _ = filelib:ensure_dir(MboxDir), 63 | Directory = filename:join([MboxDir, [Username, "/"]]), 64 | % Trailing slash is a hack to get filelib:ensure_dir/1 to always work 65 | _ = filelib:ensure_dir(Directory), 66 | Directory. 67 | 68 | mbox_base_directory() -> 69 | {ok, Home} = init:get_argument(home), 70 | filename:join([Home, "mbox/"]). 71 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [cowboy, jsx]}. 2 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.4.0">>},0}, 3 | {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.3.0">>},1}, 4 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.9.0">>},0}, 5 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},1}]}. 6 | [ 7 | {pkg_hash,[ 8 | {<<"cowboy">>, <<"F1B72FABE9C8A5FC64AC5AC85FB65474D64733D1DF52A26FAD5D4BA3D9F70A9F">>}, 9 | {<<"cowlib">>, <<"BBD58EF537904E4F7C1DD62E6AA8BC831C8183CE4EFA9BD1150164FE15BE4CAA">>}, 10 | {<<"jsx">>, <<"D2F6E5F069C00266CAD52FB15D87C428579EA4D7D73A33669E12679E203329DD">>}, 11 | {<<"ranch">>, <<"F04166F456790FEE2AC1AA05A02745CC75783C2BFB26D39FAF6AEFC9A3D3A58A">>}]} 12 | ]. 13 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/shell1.css: -------------------------------------------------------------------------------- 1 | body {margin-left:100px; 2 | border-radius:10px; 3 | padding:1em; 4 | width:320px; 5 | height:260px; 6 | border: 2px solid black; 7 | box-shadow:black 0.2em 0.2em 0.2em; 8 | } 9 | 10 | #scroll{height:10em; 11 | width:300px; 12 | font-family: "Courier New", Courier, monospace; 13 | font-weight:bold; 14 | background-color:#efefef; 15 | padding:10px; 16 | overflow-y:scroll; 17 | box-shadow:black 0.1em 0.1em 0.1em; 18 | border-radius: 10px; 19 | } 20 | 21 | #input { width:320px; 22 | box-shadow:black 0.1em 0.1em 0.1em; 23 | border-radius: 2px; 24 | } 25 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/shell1.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |

Erlang shell

18 |
19 |
20 |

21 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/src/exercise_1.app.src: -------------------------------------------------------------------------------- 1 | {application, exercise_1, 2 | [{description, ""}, 3 | {vsn, "0.1"}, 4 | {registered, []}, 5 | {modules, [ 6 | exercise_1, 7 | shell1, 8 | webserver 9 | ]}, 10 | {applications, [ 11 | kernel 12 | ,stdlib 13 | ,cowboy 14 | ]}, 15 | {mod, {exercise_1_app, []}}, 16 | {env, []} 17 | ]}. 18 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/src/exercise_1_app.erl: -------------------------------------------------------------------------------- 1 | -module(exercise_1_app). 2 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/src/rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/src/shell1.erl: -------------------------------------------------------------------------------- 1 | %% --- 2 | %% Excerpted from "Programming Erlang, Second Edition", 3 | %% published by The Pragmatic Bookshelf. 4 | %% Copyrights apply to this code. It may not be used to create training material, 5 | %% courses, books, articles, and the like. Contact us if you are in doubt. 6 | %% We make no guarantees that this code is fit for any purpose. 7 | %% Visit http://www.pragmaticprogrammer.com/titles/jaerlang2 for more book information. 8 | %%--- 9 | -module(shell1). 10 | -export([start/1, bf/2]). 11 | 12 | start(Browser) -> 13 | Browser ! #{cmd => append_div, id => scroll, 14 | txt => <<"Starting Erlang shell:
">>}, 15 | B0 = erl_eval:new_bindings(), 16 | running(Browser, B0, 1). 17 | running(Browser, B0, N) -> 18 | receive 19 | {Browser, #{entry := <<"input">>, txt := Bin}} -> 20 | {Value, B1} = string2value(binary_to_list(Bin), B0), 21 | BV = bf("~w > ~s
~p
", [N, Bin, Value]), 22 | Browser ! #{cmd => append_div, id => scroll, txt => BV}, 23 | running(Browser, B1, N+1) 24 | end. 25 | 26 | string2value(Str, Bindings0) -> 27 | case erl_scan:string(Str, 0) of 28 | {ok, Tokens, _} -> 29 | case erl_parse:parse_exprs(Tokens) of 30 | {ok, Exprs} -> 31 | {value, Val, Bindings1} = erl_eval:exprs(Exprs, Bindings0), 32 | {Val, Bindings1}; 33 | Other -> 34 | io:format("cannot parse:~p Reason=~p~n",[Tokens,Other]), 35 | {parse_error, Bindings0} 36 | end; 37 | Other -> 38 | io:format("cannot tokenise:~p Reason=~p~n",[Str,Other]) 39 | end. 40 | 41 | bf(F, D) -> 42 | list_to_binary(io_lib:format(F, D)). 43 | 44 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/src/webserver.erl: -------------------------------------------------------------------------------- 1 | -module(webserver). 2 | 3 | -export([start/1, init/2, websocket_init/1, websocket_handle/2, websocket_info/2]). 4 | 5 | start(Port) -> 6 | {ok, _} = application:ensure_all_started(cowboy), 7 | 8 | Dispatch = cowboy_router:compile( 9 | [ 10 | {'_', [{'_', webserver, []}]} 11 | ]), 12 | 13 | {ok, _} = cowboy:start_clear(webserver, [{port, Port}], #{env => #{dispatch => Dispatch}}). 14 | 15 | init(Req, State) -> 16 | {cowboy_websocket, Req, State}. 17 | 18 | websocket_init(State) -> 19 | Pid = spawn_link(shell1, start, [self()]), 20 | _ = register(shell1, Pid), 21 | {ok, State}. 22 | 23 | websocket_handle(_Frame = {text, Msg}, State) -> 24 | % Using {labels, atom} is generally unsafe as malicious clients can send 25 | % random keys and use up all the space in the atom table. In this case I 26 | % chose to use this so I could reuse the shell.erl code from the book 27 | % without having to modify it to handle binary keys. 28 | Data = jsx:decode(Msg, [{labels, atom}, return_maps]), 29 | shell1 ! {self(), Data}, 30 | {ok, State}; 31 | websocket_handle(Frame, State) -> 32 | {ok, State}. 33 | 34 | websocket_info({log, Text}, State) -> 35 | {reply, {text, Text}, State}; 36 | websocket_info(Info, State) when is_map(Info) -> 37 | % If Info is a map we assume it is a message from shell1 38 | Bin = jsx:encode([Info]), 39 | {reply, {text, Bin}, State}; 40 | websocket_info({'EXIT', _Pid, Failure}, State) -> 41 | % Show the user the crash 42 | BV = shell1:bf("#> ~p
", [Failure]), 43 | Message = #{cmd => append_div, id => scroll, txt => BV}, 44 | Bin = jsx:encode([Message]), 45 | 46 | % Restart 47 | Pid = spawn_link(shell1, start, [self()]), 48 | _ = register(shell1, Pid), 49 | {reply, {text, Bin}, State}; 50 | websocket_info(_Info, State) -> 51 | {ok, State}. 52 | -------------------------------------------------------------------------------- /chapter_18/exercise_1/websock.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Excerpted from "Programming Erlang, Second Edition", 3 | * published by The Pragmatic Bookshelf. 4 | * Copyrights apply to this code. It may not be used to create training material, 5 | * courses, books, articles, and the like. Contact us if you are in doubt. 6 | * We make no guarantees that this code is fit for any purpose. 7 | * Visit http://www.pragmaticprogrammer.com/titles/jaerlang2 for more book information. 8 | ***/ 9 | function connect(host, port, mod){ 10 | make_live_buttons(); 11 | make_live_inputs(); 12 | var ws = 'ws://' + host + ':' + port + '/websocket/' + mod; 13 | start_session(ws); 14 | 15 | } 16 | 17 | function onClose(evt) { 18 | // change the color of the display when the socket closes 19 | // so we can see it closed 20 | document.body.style.backgroundColor='#aabbcc'; 21 | } 22 | 23 | function onMessage(evt) { 24 | var json = JSON.parse(evt.data); 25 | do_cmds(json); 26 | } 27 | 28 | function onError(evt) { 29 | // if we get an error change the color of the display so we 30 | // can see we got an error 31 | document.body.style.backgroundColor='orange'; 32 | } 33 | 34 | function send(msg) { 35 | websocket.send(msg); 36 | } 37 | 38 | function start_session(wsUri){ 39 | websocket = new WebSocket(wsUri); 40 | websocket.onopen = onOpen; 41 | websocket.onclose = onClose; 42 | websocket.onmessage = onMessage; 43 | websocket.onerror = onError; 44 | return(false); 45 | } 46 | 47 | function onOpen(evt) { 48 | // console.log("connected"); 49 | } 50 | 51 | function do_cmds(objs){ 52 | // console.log('do_cmds', objs); 53 | for(var i = 0; i < objs.length; i++){ 54 | var o = objs[i]; 55 | // as a safety measure we only evaluate js that is loaded 56 | if(eval("typeof("+o.cmd+")") == "function"){ 57 | eval(o.cmd + "(o)"); 58 | } else { 59 | alert("bad_command:"+o.cmd); 60 | }; 61 | }; 62 | } 63 | 64 | function make_live_buttons(){ 65 | $(".live_button").each( 66 | function(){ 67 | var b=$(this); 68 | var txt = b.text(); 69 | b.click(function(){ 70 | send_json({'clicked':txt}); 71 | }); 72 | }); 73 | } 74 | 75 | function send_json(x){ 76 | send(JSON.stringify(x)); 77 | } 78 | 79 | // We want the inputs to send a message when we hit CR in the input 80 | 81 | function make_live_inputs(){ 82 | $(".live_input").each( 83 | function(){ 84 | var e=$(this); 85 | var id = e.attr('id'); 86 | // console.log("entry",[e,id]); 87 | e.keyup(function(ev){ 88 | if(ev.keyCode==13){ 89 | read_entry(e, id); 90 | }; 91 | }); 92 | 93 | }); 94 | } 95 | 96 | function read_entry(x, id){ 97 | var val = x.val(); 98 | x.val(" "); 99 | send_json({'entry':id, txt:val}); 100 | } 101 | 102 | // browser commands 103 | 104 | function append_div(o){ 105 | var x = $("#"+o.id); 106 | x.append(o.txt); 107 | x.animate({scrollTop: x.prop("scrollHeight") }, 1000); 108 | } 109 | 110 | function fill_div(o){ 111 | $('#'+o.id).html(o.txt); 112 | } 113 | 114 | -------------------------------------------------------------------------------- /chapter_19/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 19 2 | 3 | **1. Make key-value table ETS and DETS tables that store all exported functions from all exported modules. The key should be `{Function, Arity}`.** 4 | 5 | In `exercise_1/` there is a module named `module_exports` that contains a `store_exports/0` function. The function creates an ETS table and a DETS table and stores all exports in both of them. 6 | 7 | **2. Implement a shared ETS table. Implement a function named `count:me(Mod, Line)`. Every time it is invoked it should increment a counter for that module and line. You can call this function by adding `count:me(?MODULE, ?LINE)` to your code. Also write functions to initialize and retrieve counts.** 8 | 9 | In `exercise_2/` there is a module named `count` that exports three functions - `me/2, initialize/0, all_counters/0`. 10 | 11 | **3. Write a plagiarism detector. Hash 40 character blocks of text and store them in an ETS table. Repeat the hashing and compare each of the hashes with those stored in the ETS table.** 12 | 13 | Solution in the [exercise_3](exercise_3/) directory. The code is packaged in an escript for ease of use. The `text_files` contains several text files with some duplicate strings to exercise the detection functionality. 14 | 15 | ```bash 16 | ./plagiarism_detector text_files 17 | ``` 18 | 19 | References 20 | 21 | * http://erlang.org/pipermail/erlang-questions/2018-October/096597.html 22 | * https://github.com/jeremygoh/rabin-karp-ruby/blob/master/rabin_karp.rb 23 | -------------------------------------------------------------------------------- /chapter_19/exercise_1/module_exports.erl: -------------------------------------------------------------------------------- 1 | -module(module_exports). 2 | 3 | -export([store_exports/0]). 4 | 5 | store_exports() -> 6 | io:format("Creating tables...~n"), 7 | 8 | % Create ETS table 9 | Ets = ets:new(module_exports, [bag, protected]), 10 | 11 | % Create DETS table 12 | {ok, DetsName} = dets:open_file(?MODULE, []), 13 | 14 | % Insert function 15 | InsertIntoBoth = fun({Mod, Exports}) -> 16 | lists:map(fun(Export) -> 17 | ets:insert(Ets, {Export, Mod}), 18 | dets:insert(DetsName, {Export, Mod}) 19 | end, Exports) 20 | end, 21 | 22 | io:format("Populating tables with module exports...~n"), 23 | % Populate the tables 24 | lists:map(InsertIntoBoth, all_functions()), 25 | 26 | io:format("Complete~n"), 27 | 28 | % Close the dets table properly 29 | io:format("Closing DETS table...~n"), 30 | dets:close(DetsName), 31 | io:format("Table closed~n"), 32 | ok. 33 | 34 | loaded_modules() -> 35 | % Get all loaded modules 36 | Modules = code:all_loaded(), 37 | 38 | % Take the list of tuples and convert it to a list of module names (atoms) 39 | ModuleNames = lists:map(fun({Name, _Path}) -> 40 | Name 41 | end, Modules), 42 | ModuleNames. 43 | 44 | all_functions() -> 45 | % Returns an array of all functions from all modules. Include duplicates 46 | Modules = loaded_modules(), 47 | 48 | % Map modules and module exports to `{module, [Export]}` tuples 49 | AllExports = lists:map(fun(Mod) -> 50 | {Mod, Mod:module_info(exports)} 51 | end, Modules), 52 | lists:flatten(AllExports). 53 | -------------------------------------------------------------------------------- /chapter_19/exercise_2/count.erl: -------------------------------------------------------------------------------- 1 | -module(count). 2 | 3 | % API 4 | -export([initialize/0, me/2, all_counters/0]). 5 | 6 | -define(TABLE_NAME, counter). 7 | -define(PROCESS_NAME, counter). 8 | 9 | % It probably would have been better to just use a gen_server here. Since 10 | % this exercise is before the OTP section in the book I chose to use a simple 11 | % RPC function. 12 | 13 | % API 14 | initialize() -> 15 | register(?PROCESS_NAME, spawn(fun() -> start_loop() end)). 16 | 17 | me(Module, Line) -> 18 | rpc({increment, Module, Line}). 19 | 20 | all_counters() -> 21 | rpc(all). 22 | 23 | % Private Functions 24 | rpc(Call) -> 25 | ?PROCESS_NAME ! {self(), Call}, 26 | receive 27 | {?PROCESS_NAME, Reply} -> 28 | Reply 29 | end. 30 | 31 | start_loop() -> 32 | % Create a new ets table before the loop function beings handling requests 33 | Tab = ets:new(?TABLE_NAME, [set, public]), 34 | 35 | % Start handling requests 36 | loop(Tab). 37 | 38 | loop(Tab) -> 39 | receive 40 | {From, all} -> 41 | % We iterate over all the items in the table, adding them to a list. 42 | Result = ets:foldl(fun(Item, Acc) -> 43 | [Item|Acc] 44 | end, [], Tab), 45 | From ! {?PROCESS_NAME, Result}; 46 | {From, {increment, Module, Line}} -> 47 | % Keys are composed of modules and line numbers 48 | Key = {Module, Line}, 49 | 50 | Result = case ets:lookup(Tab, Key) of 51 | [] -> 52 | % Insert new element with a count of one if no elements match 53 | ets:insert(Tab, {Key, 1}); 54 | _ -> 55 | % We can use `update_counter` to update the counter if it exists 56 | ets:update_counter(Tab, Key, {2, 1}) 57 | end, 58 | From ! {?PROCESS_NAME, Result} 59 | end, 60 | loop(Tab). 61 | -------------------------------------------------------------------------------- /chapter_19/exercise_3/text_files/genesis_2.txt: -------------------------------------------------------------------------------- 1 | Thus the heavens and the earth were finished, and all the host of them. 2 | And on the seventh day God ended his work which he had made; and he rested on the seventh day from all his work which he had made. 3 | And God blessed the seventh day, and sanctified it: because that in it he had rested from all his work which God created and made. 4 | These are the generations of the heavens and of the earth when they were created, in the day that the LORD God made the earth and the heavens, 5 | And every plant of the field before it was in the earth, and every herb of the field before it grew: for the LORD God had not caused it to rain upon the earth, and there was not a man to till the ground. 6 | But there went up a mist from the earth, and watered the whole face of the ground. 7 | And the LORD God formed man of the dust of the ground, and breathed into his nostrils the breath of life; and man became a living soul. 8 | And the LORD God planted a garden eastward in Eden; and there he put the man whom he had formed. 9 | And out of the ground made the LORD God to grow every tree that is pleasant to the sight, and good for food; the tree of life also in the midst of the garden, and the tree of knowledge of good and evil. 10 | And a river went out of Eden to water the garden; and from thence it was parted, and became into four heads. 11 | The name of the first is Pison: that is it which compasseth the whole land of Havilah, where there is gold; 12 | And the gold of that land is good: there is bdellium and the onyx stone. 13 | And the name of the second river is Gihon: the same is it that compasseth the whole land of Ethiopia. 14 | And the name of the third river is Hiddekel: that is it which goeth toward the east of Assyria. And the fourth river is Euphrates. 15 | And the LORD God took the man, and put him into the garden of Eden to dress it and to keep it. 16 | And the LORD God commanded the man, saying, Of every tree of the garden thou mayest freely eat: 17 | But of the tree of the knowledge of good and evil, thou shalt not eat of it: for in the day that thou eatest thereof thou shalt surely die. 18 | And the LORD God said, It is not good that the man should be alone; I will make him an help meet for him. 19 | And out of the ground the LORD God formed every beast of the field, and every fowl of the air; and brought them unto Adam to see what he would call them: and whatsoever Adam called every living creature, that was the name thereof. 20 | And Adam gave names to all cattle, and to the fowl of the air, and to every beast of the field; but for Adam there was not found an help meet for him. 21 | And the LORD God caused a deep sleep to fall upon Adam and he slept: and he took one of his ribs, and closed up the flesh instead thereof; 22 | And the rib, which the LORD God had taken from man, made he a woman, and brought her unto the man. 23 | And Adam said, This is now bone of my bones, and flesh of my flesh: she shall be called Woman, because she was taken out of Man. 24 | Therefore shall a man leave his father and his mother, and shall cleave unto his wife: and they shall be one flesh. 25 | And they were both naked, the man and his wife, and were not ashamed. 26 | -------------------------------------------------------------------------------- /chapter_19/exercise_3/text_files/genesis_2_excerpt.txt: -------------------------------------------------------------------------------- 1 | And out of the ground made the LORD God to grow every tree that is pleasant to the sight, and good for food; the tree of life also in the midst of the garden, and the tree of knowledge of good and evil. 2 | And a river went out of Eden to water the garden; and from thence it was parted, and became into four heads. 3 | The name of the first is Pison: that is it which compasseth the whole land of Havilah, where there is gold; 4 | And the gold of that land is good: there is bdellium and the onyx stone. 5 | And the name of the second river is Gihon: the same is it that compasseth the whole land of Ethiopia. 6 | And the name of the third river is Hiddekel: that is it which goeth toward the east of Assyria. And the fourth river is Euphrates. 7 | -------------------------------------------------------------------------------- /chapter_19/exercise_3/text_files/genesis_3.txt: -------------------------------------------------------------------------------- 1 | Now the serpent was more subtil than any beast of the field which the LORD God had made. And he said unto the woman, Yea, hath God said, Ye shall not eat of every tree of the garden? 2 | And the woman said unto the serpent, We may eat of the fruit of the trees of the garden: 3 | But of the fruit of the tree which is in the midst of the garden, God hath said, Ye shall not eat of it, neither shall ye touch it, lest ye die. 4 | And the serpent said unto the woman, Ye shall not surely die: 5 | For God doth know that in the day ye eat thereof, then your eyes shall be opened, and ye shall be as gods, knowing good and evil. 6 | And when the woman saw that the tree was good for food, and that it was pleasant to the eyes, and a tree to be desired to make one wise, she took of the fruit thereof, and did eat, and gave also unto her husband with her; and he did eat. 7 | And the eyes of them both were opened, and they knew that they were naked; and they sewed fig leaves together, and made themselves aprons. 8 | And they heard the voice of the LORD God walking in the garden in the cool of the day: and Adam and his wife hid themselves from the presence of the LORD God amongst the trees of the garden. 9 | And the LORD God called unto Adam, and said unto him, Where art thou? 10 | And he said, I heard thy voice in the garden, and I was afraid, because I was naked; and I hid myself. 11 | And he said, Who told thee that thou wast naked? Hast thou eaten of the tree, whereof I commanded thee that thou shouldest not eat? 12 | And the man said, The woman whom thou gavest to be with me, she gave me of the tree, and I did eat. 13 | And the LORD God said unto the woman, What is this that thou hast done? And the woman said, The serpent beguiled me, and I did eat. 14 | And the LORD God said unto the serpent, Because thou hast done this, thou art cursed above all cattle, and above every beast of the field; upon thy belly shalt thou go, and dust shalt thou eat all the days of thy life: 15 | And I will put enmity between thee and the woman, and between thy seed and her seed; it shall bruise thy head, and thou shalt bruise his heel. 16 | Unto the woman he said, I will greatly multiply thy sorrow and thy conception; in sorrow thou shalt bring forth children; and thy desire shall be to thy husband, and he shall rule over thee. 17 | And unto Adam he said, Because thou hast hearkened unto the voice of thy wife, and hast eaten of the tree, of which I commanded thee, saying, Thou shalt not eat of it: cursed is the ground for thy sake; in sorrow shalt thou eat of it all the days of thy life; 18 | Thorns also and thistles shall it bring forth to thee; and thou shalt eat the herb of the field; 19 | In the sweat of thy face shalt thou eat bread, till thou return unto the ground; for out of it wast thou taken: for dust thou art, and unto dust shalt thou return. 20 | And Adam called his wife's name Eve; because she was the mother of all living. 21 | Unto Adam also and to his wife did the LORD God make coats of skins, and clothed them. 22 | And the LORD God said, Behold, the man is become as one of us, to know good and evil: and now, lest he put forth his hand, and take also of the tree of life, and eat, and live for ever: 23 | Therefore the LORD God sent him forth from the garden of Eden, to till the ground from whence he was taken. 24 | So he drove out the man; and he placed at the east of the garden of Eden Cherubims, and a flaming sword which turned every way, to keep the way of the tree of life. 25 | -------------------------------------------------------------------------------- /chapter_2/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 2 2 | 3 | ## 1. Start and stop the Erlang shell 4 | 5 | To start the shell run `erl`: 6 | 7 | $ erl 8 | Erlang R16B01 (erts-5.10.2) [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] 9 | 10 | Eshell V5.10.2 (abort with ^G) 11 | 1> 12 | 13 | Then to exit the shell run: 14 | 15 | ^C 16 | BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded 17 | (v)ersion (k)ill (D)b-tables (d)istribution 18 | 19 | Then type `a`. 20 | 21 | ## 2. Run a few commands in the shell 22 | 23 | $ erl 24 | Erlang R16B01 (erts-5.10.2) [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] 25 | 26 | Eshell V5.10.2 (abort with ^G) 27 | 1> Text = "Hello World~n". %% Here we assign a list/string to the variable `Text` 28 | "Hello World~n" 29 | 2> io:format(Text). %% Here we print the variable `Text` with io:format/1 30 | Hello World 31 | ok 32 | 3> 33 | 34 | ## 3. Make a small modification to hello.erl 35 | I updated it by allowing it to take an a single argument and greet the variable instead of `World`. See source file in exercise_3/. 36 | 37 | ## 4. Run the client and server code 38 | The source files are the exercise_2 directory. A put_file/3 function as been added to the client. The server has also been updated to handle the new command. 39 | -------------------------------------------------------------------------------- /chapter_2/exercise_3/hello.erl: -------------------------------------------------------------------------------- 1 | -module(hello). 2 | 3 | -export([start/1]). 4 | 5 | start(Name) -> 6 | io:format("Hello " ++ Name ++ "~n"). 7 | -------------------------------------------------------------------------------- /chapter_2/exercise_4/afile_client.erl: -------------------------------------------------------------------------------- 1 | -module(afile_client). 2 | 3 | -export([ls/1, get_file/2, put_file/3]). 4 | 5 | ls(Server) -> 6 | Server ! {self(), list_dir}, 7 | receive 8 | {Server, FileList} -> 9 | FileList 10 | end. 11 | 12 | get_file(Server, File) -> 13 | Server ! {self(), {get_file, File}}, 14 | receive 15 | {Server, Content} -> 16 | Content 17 | end. 18 | 19 | %% The exercise called for the addition of a put_file function 20 | put_file(Server, File, Contents) -> 21 | Server ! {self(), {put_file, File, Contents}}, 22 | receive 23 | {Server, Status} -> 24 | Status 25 | end. 26 | -------------------------------------------------------------------------------- /chapter_2/exercise_4/afile_server.erl: -------------------------------------------------------------------------------- 1 | -module(afile_server). 2 | -export([start/1, loop/1]). 3 | 4 | start(Dir) -> spawn(afile_server, loop, [Dir]). 5 | 6 | loop(Dir) -> 7 | receive 8 | {Client, list_dir} -> 9 | Client ! {self(), file:list_dir(Dir)}; 10 | {Client, {get_file, File}} -> 11 | Full = filename:join(Dir, File), 12 | Client ! {self(), file:read_file(Full)}; 13 | %% The exercise called for the addition of a put_file command 14 | {Client, {put_file, Filename, Contents}} -> 15 | Full = filename:join(Dir, Filename), 16 | Client ! {self(), file:write_file(Full, Contents)} 17 | end, 18 | loop(Dir). 19 | -------------------------------------------------------------------------------- /chapter_20/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 20 2 | 3 | ## 1. Build a database for a website. Database needs to have three tables: users, tips and abuse. 4 | `erlang_tips.erl` is stored in `exercise_1/`. Example usage: 5 | 6 | 1> erlang_tips:module_info(exports). 7 | [{all_users,0}, 8 | {get_user,1}, 9 | {add_user,3}, 10 | {all_tips,0}, 11 | {get_tip,1}, 12 | {add_tip,3}, 13 | {all_abuse,0}, 14 | {get_abuse,1}, 15 | {add_abuse,2}, 16 | {start_database,0}, 17 | {create_database,0}, 18 | {reset_tables,0}, 19 | {module_info,0}, 20 | {module_info,1}] 21 | 2> erlang_tips:create_database(). 22 | ok 23 | 3> erlang_tips:add_tip("google.com", "Use this site to find stuff", undefined). 24 | {atomic,ok} 25 | 4> erlang_tips:add_tip("google.com", "Use this site to find stuff", undefined). 26 | {atomic,ok} 27 | 5> erlang_tips:add_tip("stratus3d.com", "This site has tons of info on Erlang!", undefined). 28 | {atomic,ok} 29 | 6> erlang_tips:all_tips(). 30 | [{tip,"stratus3d.com", 31 | "This site has tons of info on Erlang!",undefined}, 32 | {tip,"google.com","This site is a fake",undefined}] 33 | 5> 34 | 35 | ## 2. Take the previous exercise and get it run with ram and disk copies on two nodes. 36 | `erlang_tips.erl` is stored in `exercise_2/`. To run the `erlang_tips` database on two nodes open up two terminal sessions and start two Erlang nodes. The first: 37 | 38 | cd exercise_2/ 39 | erl -sname node1@localhost -setcookie cookie 40 | 41 | The second: 42 | 43 | cd exercise_2/ 44 | erl -sname node2@localhost -setcookie cookie 45 | 46 | Note that the node names are hardcoded at the top of the `exercise_2/erlang_tips.erl` module. If you need to change them alter the `NODE_NAMES` macro (as well as the commands shown above). Once both nodes are running create the database and run a few commands on the first node: 47 | 48 | (node1@localhost)1> erlang_tips:create_database(). 49 | ok 50 | (node1@localhost)2> erlang_tips:add_tip("stratus3d.com", "This site contains a lot of great articles on Erlang!", undefined). 51 | {atomic,ok} 52 | (node1@localhost)3> erlang_tips:add_tip("google.com", "Use this site to find stuff", undefined). 53 | {atomic,ok} 54 | (node1@localhost)4> erlang_tips:add_tip("google.com", "Use this site to find stuff", undefined). 55 | {atomic,ok} 56 | (node1@localhost)5> 57 | 58 | Then run the `all_tips` function to retrieve the tips on node 2: 59 | 60 | (node2@localhost)1> erlang_tips:all_tips(). 61 | [{tip,"stratus3d.com", 62 | "This site contains a lot of great articles on Erlang!", 63 | undefined}, 64 | {tip,"google.com","Use this site to find stuff",undefined}] 65 | (node2@localhost)2> 66 | 67 | ## 3. Write function to reject tip submissions if the user has submitted more than 10 tips in a one-day period. Time queries. 68 | `erlang_tips.erl` for exercise 3 is stored in `exercise_3/`. To run this version of the `erlang_tips` you only need to run one node. 69 | -------------------------------------------------------------------------------- /chapter_20/exercise_1/erlang_tips.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_tips). 2 | 3 | -include_lib("stdlib/include/qlc.hrl"). 4 | 5 | -record(user, {name, email, password}). 6 | -record(tip, {url, description, date}). 7 | -record(abuse, {ip_address, num_visits}). 8 | 9 | -export([all_users/0, get_user/1, add_user/3, all_tips/0, get_tip/1, add_tip/3, 10 | all_abuse/0, get_abuse/1, add_abuse/2, start_database/0, 11 | create_database/0, reset_tables/0]). 12 | 13 | %%%=================================================================== 14 | %%% Primary API 15 | %%%=================================================================== 16 | all_users() -> 17 | % Get all users in the user table 18 | do(qlc:q([X || X <- mnesia:table(user)])). 19 | 20 | get_user(Email) -> 21 | % Select a user from the user table by email address 22 | do(qlc:q([X || X <- mnesia:table(user), X#user.email == Email])). 23 | 24 | add_user(Name, Email, Password) -> 25 | % Create a user record and store it in the database 26 | User = #user{name=Name, email=Email, password=Password}, 27 | F = fun() -> 28 | mnesia:write(User) 29 | end, 30 | mnesia:transaction(F). 31 | 32 | all_tips() -> 33 | % Get all tips in the tip table 34 | do(qlc:q([X || X <- mnesia:table(tip)])). 35 | 36 | get_tip(Url) -> 37 | % Select a tip from the tip table by url 38 | do(qlc:q([X || X <- mnesia:table(tip), X#tip.url == Url])). 39 | 40 | add_tip(Url, Description, Date) -> 41 | % Create a tip record and store it in the database 42 | Tip = #tip{url=Url, description=Description, date=Date}, 43 | F = fun() -> 44 | mnesia:write(Tip) 45 | end, 46 | mnesia:transaction(F). 47 | 48 | all_abuse() -> 49 | % Get all abuse records in the abuse table 50 | do(qlc:q([X || X <- mnesia:table(abuse)])). 51 | 52 | get_abuse(IpAddress) -> 53 | % Select an abuse record from the abuse table by IP address 54 | do(qlc:q([X || X <- mnesia:table(abuse), X#abuse.ip_address == IpAddress])). 55 | 56 | add_abuse(IpAddress, NumVisits) -> 57 | % Create an abuse record and store it in the database 58 | Abuse = #abuse{ip_address=IpAddress, num_visits=NumVisits}, 59 | F = fun() -> 60 | mnesia:write(Abuse) 61 | end, 62 | mnesia:transaction(F). 63 | 64 | start_database() -> 65 | % Get the database in a state where we can query it 66 | mnesia:start(), 67 | mnesia:wait_for_tables([user,tip,abuse], 20000). 68 | 69 | create_database() -> 70 | % Create schema 71 | ok = mnesia:create_schema([node()]), 72 | 73 | % Start mnesia if it's not already started 74 | mnesia:start(), 75 | 76 | % Table Storage 77 | DiskCopies = {disc_copies, [node()]}, 78 | 79 | % Create tables 80 | {atomic, ok} = mnesia:create_table(user, [{attributes, record_info(fields, user)}, DiskCopies]), 81 | {atomic, ok} = mnesia:create_table(tip, [{attributes, record_info(fields, tip)}, DiskCopies]), 82 | {atomic, ok} = mnesia:create_table(abuse, [{attributes, record_info(fields, abuse)}, DiskCopies]), 83 | ok. 84 | 85 | reset_tables() -> 86 | % Empty all the tables in the database 87 | mnesia:clear_table(user), 88 | mnesia:clear_table(tip), 89 | mnesia:clear_table(abuse). 90 | 91 | %%%=================================================================== 92 | %%% Private functions 93 | %%%=================================================================== 94 | do(Q) -> 95 | F = fun() -> qlc:e(Q) end, 96 | {atomic, Val} = mnesia:transaction(F), 97 | Val. 98 | -------------------------------------------------------------------------------- /chapter_21/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 21 2 | 3 | ## 1. Add an error to dict.erl. Compile the module. 4 | I have altered `dict.erl` so a certain line in it will cause it to crash. `dict.erl` is stored in the `exercise_1/` directory. 5 | 6 | ## 2. Write a simple test module that calls dict in various ways to uncover the error. 7 | Test module is in `exercise_2/`. In this directory, run: 8 | 9 | erlc exercise_1/dict.erl exercise_2/dict_test.erl 10 | erl 11 | 12 | And then in the Erlang console run the tests: 13 | 14 | dict_test:test(). 15 | 16 | 17 | ## 3. Use the code coverage tool to see how many lines in the dict module are being executed by the test module. 18 | The code coverage script is stored in the `exercise_3` directory. To run the code coverage analysis compile the code and start an erl console: 19 | 20 | erlc exercise_1/dict.erl exercise_2/dict_test.erl exercise_3/dict_test_coverage.erl 21 | erl 22 | 23 | And then in the Erlang console run the command to generate the code coverage results for the tests written in exercise 2: 24 | 25 | dict_test_coverage:coverage(). 26 | 27 | ## 4. Use the dbg module to find the error in the dict module 28 | The dbg script is stored in the `exercise_4` directory. To run the dbg tracing on the dict module compile the code and start an erl console: 29 | 30 | erlc exercise_1/dict.erl exercise_2/dict_test.erl exercise_3/dict_test_coverage.erl 31 | erl 32 | 33 | And then in the Erlang console run: 34 | 35 | dict_test_debug:trace(). 36 | -------------------------------------------------------------------------------- /chapter_21/exercise_2/dict_test.erl: -------------------------------------------------------------------------------- 1 | -module(dict_test). 2 | 3 | -export([test/0]). 4 | 5 | % For exercise 21.2 we need to test the dict module in as many ways as possible. 6 | % The tests here will uncover the error present in src/dict.erl. 7 | 8 | test() -> 9 | % Create dictionary 10 | Dict = dict:new(), 11 | 12 | % Add to dictionary 13 | Dict2 = dict:append(somekey, someval, Dict), 14 | 15 | % Add another item to dictionary 16 | Dict3 = dict:append(anotherkey, anotherval, Dict2), 17 | 18 | % Fetch item from dictionary 19 | _Value = dict:fetch(somekey, Dict3), 20 | 21 | % Update item in dictionary 22 | Dict4 = dict:update(somekey, fun (_) -> 23 | new_elem 24 | end, Dict3), 25 | 26 | % Find item in dictionary 27 | {ok, _Value2} = dict:find(anotherkey, Dict4), 28 | 29 | % Delete item in dictionary 30 | _Dict5 = dict:erase(anotherkey, Dict4). 31 | -------------------------------------------------------------------------------- /chapter_21/exercise_3/dict_test_coverage.erl: -------------------------------------------------------------------------------- 1 | -module(dict_test_coverage). 2 | 3 | -export([coverage/0]). 4 | 5 | % Check the code coverage of the dict_test:test/0 test on the dict module. 6 | 7 | coverage() -> 8 | % Start the code coverage tool 9 | {ok, _CoverPid} = cover:start(), 10 | 11 | % We have to do this since the dict module is most likely already loaded. 12 | ok = code:unstick_dir("."), 13 | 14 | % Compile the dict module we are testing with cover 15 | {ok, _Mod} = cover:compile("exercise_1/dict.erl"), 16 | 17 | % Run tests which exercise the dict module 18 | dict_test:test(), 19 | 20 | % Write the results to a file 21 | {ok, _File} = cover:analyse_to_file(dict). 22 | -------------------------------------------------------------------------------- /chapter_21/exercise_4/dict_test_debug.erl: -------------------------------------------------------------------------------- 1 | -module(dict_test_debug). 2 | 3 | -export([trace/0]). 4 | 5 | trace() -> 6 | {ok, _Tracer} = dbg:tracer(), 7 | 8 | {ok, _Desc} = dbg:p(all, c), 9 | 10 | {ok, _Tpl} = dbg:tpl(dict, []), 11 | 12 | dict_test:test(), 13 | 14 | ok = dbg:stop(), 15 | 16 | ok. 17 | 18 | 19 | % -module(math). 20 | % -export([sum/2, diff/2]). 21 | % 22 | % sum(A, B) -> A + B. 23 | % diff(A, B) -> A - B. 24 | % 25 | % Just start the tracer: 26 | % > dbg:tracer() 27 | % 28 | % Say the tracer that you are interested in all calls for all processes: 29 | % > dbg:p(all, c) 30 | % 31 | % Finally, say it that you want to trace the function sum from the module math: 32 | % > dbg:tpl(math, sum, []) 33 | % 34 | % Now, try to call the function, as usual. The tracer is active! 35 | % > math:sum(2, 3). 36 | % 37 | % To stop the trace: 38 | % > dbg:stop(). 39 | % 40 | % To trace all the functions within the module math: 41 | % > dbg:tpl(math, []) 42 | % 43 | % To trace the return value for a given function: 44 | % > dbg:tpl(math, sum, dbg:fun2ms(fun(_) -> return_trace() end)). 45 | -------------------------------------------------------------------------------- /chapter_22/README.md: -------------------------------------------------------------------------------- 1 | # Exercises 22 2 | 3 | **1. Implement basic job center functionality with the following interface:** 4 | 5 | * `job_centre:start_link() -> true.` 6 | * `job_centre:add_job(F) -> JobNumber.` 7 | * `job_centre:work_wanted() -> {JobNumber, F} | no.` 8 | * `job_centre:job_done(JobNumber)` 9 | 10 | Solution in the [exercise_1/](exercise_1/) directory. 11 | 12 | ``` 13 | erl 14 | 1> job_centre:start_link(). 15 | {ok,<0.67.0>} 16 | 2> job_centre:add_job(fun() -> io:format("working on job") end). 17 | 1 18 | 3> {JobID, JobFun} = job_centre:work_wanted(). 19 | {1,#Fun} 20 | 4> JobFun(). 21 | working on jobok 22 | 5> job_centre:job_done(JobID). 23 | ok 24 | ``` 25 | 26 | **2. Add function named `job_centre:statistics/0` that reports the status of the jobs in the queue.** 27 | 28 | Solution in the [exercise_2/](exercise_2/) directory. 29 | 30 | ``` 31 | erl 32 | 1> job_centre:start_link(). 33 | {ok,<0.67.0>} 34 | 2> job_centre:add_job(fun() -> io:format("Working on job 1") end). 35 | 1 36 | 3> job_centre:add_job(fun() -> io:format("Working on job 2") end). 37 | 2 38 | 4> {JobID, JobFun} = job_centre:work_wanted(). 39 | {1,#Fun} 40 | 5> JobFun(). 41 | Working on job 1ok 42 | 6> job_centre:job_done(JobID). 43 | not_found 44 | 7> job_centre:statistics(). 45 | [{1,in_progress},{2,pending}] 46 | ``` 47 | 48 | **3. Add code to monitor the workers. If a worker dies makesure the jobs it was doing are returned to the queue of waiting jobs.** 49 | 50 | Solution in the [exercise_3/](exercise_3/) directory. 51 | 52 | ``` 53 | erl 54 | 1> job_centre:start_link(). 55 | {ok,<0.67.0>} 56 | 2> job_centre:add_job(fun() -> io:format("Working on job 1") end). 57 | 1 58 | 3> job_centre:add_job(fun() -> io:format("Working on job 2") end). 59 | 2 60 | 4> spawn(fun() -> 61 | {JobID, JobFun} = job_centre:work_wanted(), 62 | JobFun(), 63 | exit(self(), test_failure) 64 | end). 65 | Working on job 1<0.71.0> 66 | Worker process <0.71.0> crashed with reason test_failureDOWN message from unknown process 67 | 5> {JobID, JobFun} = job_centre:work_wanted(). 68 | {1,#Fun} 69 | 6> JobFun(). 70 | Working on job 1ok 71 | ``` 72 | 73 | **4. Check for lazy workers. Change the work wanted function to return `{JobNumber, JobTime, F}` where JobTime is the number of seconds that the worker as to complete the job by. At `JobTime - 1`, the server should send a `hurry_up` message to the worker if it has not finished the job. And at time `JobTime + 1`, it should kill the worker process with an `exit(Pid, youre_fired)` call.** 74 | 75 | Solution in the [exercise_4/](exercise_4/) directory. 76 | ``` 77 | erl 78 | 1> job_centre:start_link(). 79 | {ok,<0.67.0>} 80 | 2> job_centre:add_job(fun() -> io:format("Working on job 1") end). 81 | 1 82 | 3> job_centre:add_job(fun() -> io:format("Working on job 2") end). 83 | 2 84 | 4> spawn(fun() -> 85 | {JobID, JobFun} = job_centre:work_wanted(), 86 | JobFun(), 87 | receive 88 | Msg -> 89 | io:format("~p", [Msg]) 90 | after 3000 -> 91 | ok 92 | end 93 | end). 94 | Working on job 1<0.71.0> 95 | Worker process <0.71.0> crashed with reason test_failureDOWN message from unknown process 96 | 5> job_centre:statistics(). 97 | [{1,pending},{2,pending}] 98 | ``` 99 | 100 | **5. Implement a trade union server to monitor the workers. Check that they are not fired without being sent a warning.** 101 | 102 | Not going to implement this as it's not a design pattern I've seen regularly used in OTP applications. 103 | -------------------------------------------------------------------------------- /chapter_22/exercise_1/job_centre.erl: -------------------------------------------------------------------------------- 1 | -module(job_centre). 2 | 3 | -behaviour(gen_server). 4 | 5 | % API 6 | -export([start_link/0, add_job/1, work_wanted/0, job_done/1]). 7 | 8 | %% gen_server callbacks 9 | -export([init/1, 10 | handle_call/3, 11 | handle_cast/2, 12 | handle_info/2, 13 | terminate/2, 14 | code_change/3]). 15 | 16 | 17 | % API functions 18 | start_link() -> 19 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 20 | 21 | add_job(Fun) -> 22 | gen_server:call(?MODULE, {add_job, Fun}). 23 | 24 | work_wanted() -> 25 | gen_server:call(?MODULE, checkout_job). 26 | 27 | job_done(JobId) -> 28 | gen_server:call(?MODULE, {remove_job, JobId}). 29 | 30 | % gen_server callbacks 31 | init([]) -> 32 | % Initial the gen_server state with an integer for future IDs and a ets 33 | % table 34 | {ok, {0, ets:new(?MODULE,[])}}. 35 | 36 | handle_call({add_job, Fun}, _From, {LastId, Tab}) -> 37 | % Generate another job ID by incrementing the last one 38 | JobId = LastId + 1, 39 | 40 | % Insert the ID and work function into the ets table 41 | true = ets:insert(Tab, job(JobId, Fun)), 42 | {reply, JobId, {JobId, Tab}}; 43 | handle_call(checkout_job, _From, {_, Tab} = State) -> 44 | % Find a job 45 | case ets:match(Tab, {'$1', '$2', '_'}) of 46 | [] -> 47 | % If none return `no` 48 | {reply, no, State}; 49 | [[JobId, JobFun]|_] -> 50 | % If one is found mark it as in_progess and return it to the caller 51 | ets:insert(Tab, job(JobId, JobFun, in_progress)), 52 | {reply, {JobId, JobFun}, State} 53 | end; 54 | handle_call({remove_job, JobId}, _From, {_, Tab} = State) -> 55 | % Delete the job tuple from the ets table 56 | ets:delete(Tab, JobId), 57 | {reply, ok, State}. 58 | 59 | handle_cast(_Msg, State) -> 60 | {noreply, State}. 61 | 62 | handle_info(_Info, State) -> 63 | {noreply, State}. 64 | 65 | terminate(_Reason, _State) -> 66 | ok. 67 | 68 | code_change(_OldVsn, State, _Extra) -> 69 | {ok, State}. 70 | 71 | % Helper functions for manipulating job state 72 | job(JobId, JobFun) -> 73 | job(JobId, JobFun, pending). 74 | 75 | job(JobId, JobFun, State) -> 76 | {JobId, JobFun, State}. 77 | -------------------------------------------------------------------------------- /chapter_22/exercise_2/job_centre.erl: -------------------------------------------------------------------------------- 1 | -module(job_centre). 2 | 3 | -behaviour(gen_server). 4 | 5 | % API 6 | -export([start_link/0, add_job/1, work_wanted/0, job_done/1, statistics/0]). 7 | 8 | %% gen_server callbacks 9 | -export([init/1, 10 | handle_call/3, 11 | handle_cast/2, 12 | handle_info/2, 13 | terminate/2, 14 | code_change/3]). 15 | 16 | 17 | % API functions 18 | start_link() -> 19 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 20 | 21 | add_job(Fun) -> 22 | gen_server:call(?MODULE, {add_job, Fun}). 23 | 24 | work_wanted() -> 25 | gen_server:call(?MODULE, checkout_job). 26 | 27 | job_done(JobId) -> 28 | gen_server:call(?MODULE, {remove_job, JobId}). 29 | 30 | statistics() -> 31 | gen_server:call(?MODULE, get_statistics). 32 | 33 | % gen_server callbacks 34 | init([]) -> 35 | % Initial the gen_server state with an integer for future IDs and a ets 36 | % table 37 | {ok, {0, ets:new(?MODULE,[])}}. 38 | 39 | handle_call({add_job, Fun}, _From, {LastId, Tab}) -> 40 | % Generate another job ID by incrementing the last one 41 | JobId = LastId + 1, 42 | 43 | % Insert the ID and work function into the ets table 44 | true = ets:insert(Tab, job(JobId, Fun)), 45 | {reply, JobId, {JobId, Tab}}; 46 | handle_call(checkout_job, _From, {_, Tab} = State) -> 47 | % Find a job 48 | case ets:match(Tab, {'$1', '$2', '_'}) of 49 | [] -> 50 | % If none return `no` 51 | {reply, no, State}; 52 | [[JobId, JobFun]|_] -> 53 | % If one is found mark it as in_progess and return it to the caller 54 | ets:insert(Tab, job(JobId, JobFun, in_progress)), 55 | {reply, {JobId, JobFun}, State} 56 | end; 57 | handle_call({remove_job, JobId}, _From, {_, Tab} = State) -> 58 | % Mark the job as done in the ETS table 59 | case ets:lookup(Tab, JobId) of 60 | [] -> 61 | {reply, not_found, State}; 62 | [{JobId, JobFun, _State}|_] -> 63 | ets:insert(Tab, job(JobId, JobFun, finished)), 64 | {reply, ok, State} 65 | end; 66 | handle_call(get_statistics, _From, {_, Tab} = State) -> 67 | % Dump job data from ets table 68 | Jobs = ets:tab2list(Tab), 69 | 70 | % Format the data and return the state of each job 71 | Results = lists:map(fun({JobID, _JobFun, JobState}) -> 72 | {JobID, JobState} 73 | end, Jobs), 74 | {reply, Results, State}. 75 | 76 | handle_cast(_Msg, State) -> 77 | {noreply, State}. 78 | 79 | handle_info(_Info, State) -> 80 | {noreply, State}. 81 | 82 | terminate(_Reason, _State) -> 83 | ok. 84 | 85 | code_change(_OldVsn, State, _Extra) -> 86 | {ok, State}. 87 | 88 | % Helper functions for manipulating job state 89 | job(JobId, JobFun) -> 90 | job(JobId, JobFun, pending). 91 | 92 | job(JobId, JobFun, State) -> 93 | {JobId, JobFun, State}. 94 | -------------------------------------------------------------------------------- /chapter_23/exercise_1/lib_lin.erl: -------------------------------------------------------------------------------- 1 | %% --- 2 | %% Excerpted from "Programming Erlang, Second Edition", 3 | %% published by The Pragmatic Bookshelf. 4 | %% Copyrights apply to this code. It may not be used to create training material, 5 | %% courses, books, articles, and the like. Contact us if you are in doubt. 6 | %% We make no guarantees that this code is fit for any purpose. 7 | %% Visit http://www.pragmaticprogrammer.com/titles/jaerlang2 for more book information. 8 | %%--- 9 | -module(lib_lin). 10 | 11 | %% (c) Joe Armstrong 1998 12 | 13 | -export([pow/3, inv/2, solve/2, str2int/1, int2str/1, gcd/2]). 14 | 15 | %% pow(A, B, M) => (A^B) mod M 16 | %% examples pow(9726,3533,11413) = 5761 17 | %% pow(5971,6597,11413) = 9726 18 | 19 | pow(A, 1, M) -> 20 | A rem M; 21 | pow(A, 2, M) -> 22 | A*A rem M; 23 | pow(A, B, M) -> 24 | B1 = B div 2, 25 | B2 = B - B1, 26 | %% B2 = B1 or B1+1 27 | P = pow(A, B1, M), 28 | case B2 of 29 | B1 -> (P*P) rem M; 30 | _ -> (P*P*A) rem M 31 | end. 32 | 33 | %% inv(A, B) = C | no_inverse 34 | %% computes C such that 35 | %% A*C mod B = 1 36 | %% computes A^-1 mod B 37 | %% examples inv(28, 75) = 67. 38 | %% inv(3533, 11200) = 6597 39 | %% inv(6597, 11200) = 3533 40 | 41 | inv(A, B) -> 42 | case solve(A, B) of 43 | {X, _} -> 44 | if X < 0 -> X + B; 45 | true -> X 46 | end; 47 | _ -> 48 | no_inverse 49 | end. 50 | 51 | %% solve(A, B) => {X, Y} | insoluble 52 | %% solve the linear congruence 53 | %% A * X - B * Y = 1 54 | 55 | solve(A, B) -> 56 | case catch s(A,B) of 57 | insoluble -> insoluble; 58 | {X, Y} -> 59 | case A * X - B * Y of 60 | 1 -> {X, Y}; 61 | _Other -> error 62 | end 63 | end. 64 | 65 | s(_, 0) -> throw(insoluble); 66 | s(_, 1) -> {0, -1}; 67 | s(_, -1) -> {0, 1}; 68 | s(A, B) -> 69 | K1 = A div B, 70 | K2 = A - K1*B, 71 | {Tmp, X} = s(B, -K2), 72 | {X, K1 * X - Tmp}. 73 | 74 | 75 | 76 | %% converts a string to a base 256 integer 77 | %% converts a base 256 integer to a string 78 | 79 | str2int(Str) -> str2int(Str, 0). 80 | 81 | str2int([H|T], N) -> str2int(T, N*256+H); 82 | str2int([], N) -> N. 83 | 84 | int2str(N) -> int2str(N, []). 85 | 86 | int2str(N, L) when N =< 0 -> L; 87 | int2str(N, L) -> 88 | N1 = N div 256, 89 | H = N - N1 * 256, 90 | int2str(N1, [H|L]). 91 | 92 | %% greatest common devisor 93 | 94 | gcd(A, B) when A < B -> gcd(B, A); 95 | gcd(A, 0) -> A; 96 | gcd(A, B) -> gcd(B, A rem B). 97 | -------------------------------------------------------------------------------- /chapter_23/exercise_1/lib_primes.erl: -------------------------------------------------------------------------------- 1 | %% --- 2 | %% Excerpted from "Programming Erlang, Second Edition", 3 | %% published by The Pragmatic Bookshelf. 4 | %% Copyrights apply to this code. It may not be used to create training material, 5 | %% courses, books, articles, and the like. Contact us if you are in doubt. 6 | %% We make no guarantees that this code is fit for any purpose. 7 | %% Visit http://www.pragmaticprogrammer.com/titles/jaerlang2 for more book information. 8 | %%--- 9 | -module(lib_primes). 10 | -export([make_prime/1, is_prime/1, make_random_int/1]). 11 | 12 | make_prime(1) -> %%

cannot read:", File, "
"] 79 | end. 80 | -------------------------------------------------------------------------------- /chapter_25/exercise_2/src/exercise_2.app.src: -------------------------------------------------------------------------------- 1 | {application, exercise_2, 2 | [{description, ""}, 3 | {vsn, "0.1"}, 4 | {registered, []}, 5 | {modules, [ 6 | cgi_web_server 7 | ]}, 8 | {applications, [ 9 | kernel 10 | ,stdlib 11 | ,cowboy 12 | ]}, 13 | {mod, {exercise_1_app, []}}, 14 | {env, []} 15 | ]}. 16 | -------------------------------------------------------------------------------- /chapter_25/exercise_2/src/ping_pong.erl: -------------------------------------------------------------------------------- 1 | -module(ping_pong). 2 | 3 | -export([ping/1]). 4 | 5 | % Sample CGI function 6 | ping(_Arg) -> 7 | [<<"pong">>]. 8 | -------------------------------------------------------------------------------- /chapter_25/exercise_3/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /chapter_25/exercise_3/rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [cowboy, jsx]}. 2 | -------------------------------------------------------------------------------- /chapter_25/exercise_3/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.4.0">>},0}, 3 | {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.3.0">>},1}, 4 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.9.0">>},0}, 5 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},1}]}. 6 | [ 7 | {pkg_hash,[ 8 | {<<"cowboy">>, <<"F1B72FABE9C8A5FC64AC5AC85FB65474D64733D1DF52A26FAD5D4BA3D9F70A9F">>}, 9 | {<<"cowlib">>, <<"BBD58EF537904E4F7C1DD62E6AA8BC831C8183CE4EFA9BD1150164FE15BE4CAA">>}, 10 | {<<"jsx">>, <<"D2F6E5F069C00266CAD52FB15D87C428579EA4D7D73A33669E12679E203329DD">>}, 11 | {<<"ranch">>, <<"F04166F456790FEE2AC1AA05A02745CC75783C2BFB26D39FAF6AEFC9A3D3A58A">>}]} 12 | ]. 13 | -------------------------------------------------------------------------------- /chapter_25/exercise_3/src/cgi_web_server.erl: -------------------------------------------------------------------------------- 1 | %% --- 2 | %% Excerpted from "Programming Erlang, Second Edition", 3 | %% published by The Pragmatic Bookshelf. 4 | %% Copyrights apply to this code. It may not be used to create training material, 5 | %% courses, books, articles, and the like. Contact us if you are in doubt. 6 | %% We make no guarantees that this code is fit for any purpose. 7 | %% Visit http://www.pragmaticprogrammer.com/titles/jaerlang2 for more book information. 8 | %%--- 9 | -module(cgi_web_server). 10 | 11 | -export([start/0, start_from_shell/1, init/2, handle1/3, terminate/3]). 12 | 13 | -define(MODULE_WHITELIST, [ping_pong]). 14 | 15 | start() -> 16 | start(12345). 17 | 18 | start_from_shell([PortAsAtom]) -> 19 | PortAsInt = list_to_integer(atom_to_list(PortAsAtom)), 20 | start(PortAsInt). 21 | %% 22 | 23 | start(Port) -> 24 | {ok, _} = application:ensure_all_started(cowboy), 25 | Dispatch = cowboy_router:compile( 26 | [ 27 | %% {URIHost, list({URIPath, Handler, Opts})} 28 | {'_', [{'_', cgi_web_server, #{}}]} 29 | ]), 30 | cowboy:start_clear(cgi_web_server, 31 | [{port, Port}], 32 | #{env => #{dispatch => Dispatch}} 33 | ). 34 | 35 | init(Req, State) -> 36 | Path = cowboy_req:path(Req), 37 | handle1(Path, Req, State). 38 | 39 | handle1(<<"/cgi">>, Req, State) -> 40 | Args = cowboy_req:parse_qs(Req), 41 | {ok, Bin, Req2} = cowboy_req:read_body(Req), 42 | Val = case Bin of 43 | <<>> -> 44 | <<>>; 45 | _ -> 46 | io:format("~p", [Bin]), 47 | jsx:decode(Bin) 48 | end, 49 | Response = call(Args, Val), 50 | Json = jsx:encode(Response), 51 | Req3 = cowboy_req:reply(200, #{}, Json, Req2), 52 | {ok, Req3, State}; 53 | handle1(Path, Req, State) -> 54 | Response = read_file(Path), 55 | Req1 = cowboy_req:reply(200, #{}, Response, Req), 56 | {ok, Req1, State}. 57 | 58 | terminate(_Reason, _Req, _State) -> 59 | ok. 60 | 61 | % Private functions 62 | call([{<<"mod">>,MB},{<<"func">>,FB}], X) -> 63 | % Use list_to_existing_atom to prevent invalid parameters from flooding the 64 | % atom table 65 | Mod = list_to_existing_atom(binary_to_list(MB)), 66 | % Check if module is in whitelist 67 | case lists:member(Mod, ?MODULE_WHITELIST) of 68 | true -> 69 | % Use list_to_existing_atom to prevent invalid parameters from 70 | % flooding the atom table 71 | Func = list_to_existing_atom(binary_to_list(FB)), 72 | apply(Mod, Func, [X]); 73 | _ -> 74 | % Return an error if module not in whitelist 75 | [{error, invalid_module}] 76 | end. 77 | 78 | read_file(Path) -> 79 | % Invoke safe_relative_path on the path from the client to prevent directory 80 | % traversal attacks 81 | SafePath = filename:safe_relative_path(Path), 82 | case file:read_file(SafePath) of 83 | {ok, Bin} -> Bin; 84 | _ -> 85 | % HTML escape path to prevent injection of HTML into the error page 86 | % markup 87 | ["
cannot read:", xmerl_lib:export_text(Path), "
"] 88 | end. 89 | -------------------------------------------------------------------------------- /chapter_25/exercise_3/src/exercise_2.app.src: -------------------------------------------------------------------------------- 1 | {application, exercise_2, 2 | [{description, ""}, 3 | {vsn, "0.1"}, 4 | {registered, []}, 5 | {modules, [ 6 | cgi_web_server 7 | ]}, 8 | {applications, [ 9 | kernel 10 | ,stdlib 11 | ,cowboy 12 | ]}, 13 | {mod, {exercise_1_app, []}}, 14 | {env, []} 15 | ]}. 16 | -------------------------------------------------------------------------------- /chapter_25/exercise_3/src/ping_pong.erl: -------------------------------------------------------------------------------- 1 | -module(ping_pong). 2 | 3 | -export([ping/1]). 4 | 5 | % Sample CGI function 6 | ping(_Arg) -> 7 | [<<"pong">>]. 8 | -------------------------------------------------------------------------------- /chapter_25/exercise_4/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /chapter_25/exercise_4/rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [cowboy, jsx]}. 2 | -------------------------------------------------------------------------------- /chapter_25/exercise_4/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.4.0">>},0}, 3 | {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.3.0">>},1}, 4 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.9.0">>},0}, 5 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},1}]}. 6 | [ 7 | {pkg_hash,[ 8 | {<<"cowboy">>, <<"F1B72FABE9C8A5FC64AC5AC85FB65474D64733D1DF52A26FAD5D4BA3D9F70A9F">>}, 9 | {<<"cowlib">>, <<"BBD58EF537904E4F7C1DD62E6AA8BC831C8183CE4EFA9BD1150164FE15BE4CAA">>}, 10 | {<<"jsx">>, <<"D2F6E5F069C00266CAD52FB15D87C428579EA4D7D73A33669E12679E203329DD">>}, 11 | {<<"ranch">>, <<"F04166F456790FEE2AC1AA05A02745CC75783C2BFB26D39FAF6AEFC9A3D3A58A">>}]} 12 | ]. 13 | -------------------------------------------------------------------------------- /chapter_25/exercise_4/src/exercise_4.app.src: -------------------------------------------------------------------------------- 1 | {application, exercise_4, 2 | [{description, ""}, 3 | {vsn, "0.1"}, 4 | {registered, []}, 5 | {modules, [ 6 | cgi_web_server 7 | ]}, 8 | {applications, [ 9 | kernel 10 | ,stdlib 11 | ,cowboy 12 | ]}, 13 | {env, []} 14 | ]}. 15 | -------------------------------------------------------------------------------- /chapter_25/exercise_4/src/ping_pong.erl: -------------------------------------------------------------------------------- 1 | -module(ping_pong). 2 | 3 | -export([ping/1]). 4 | 5 | % Sample CGI function 6 | ping(_Arg) -> 7 | [<<"pong">>]. 8 | -------------------------------------------------------------------------------- /chapter_25/exercise_4/src/rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /chapter_25/exercise_5/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /chapter_25/exercise_5/rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [cowboy, jsx]}. 2 | -------------------------------------------------------------------------------- /chapter_25/exercise_5/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.4.0">>},0}, 3 | {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.3.0">>},1}, 4 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.9.0">>},0}, 5 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},1}]}. 6 | [ 7 | {pkg_hash,[ 8 | {<<"cowboy">>, <<"F1B72FABE9C8A5FC64AC5AC85FB65474D64733D1DF52A26FAD5D4BA3D9F70A9F">>}, 9 | {<<"cowlib">>, <<"BBD58EF537904E4F7C1DD62E6AA8BC831C8183CE4EFA9BD1150164FE15BE4CAA">>}, 10 | {<<"jsx">>, <<"D2F6E5F069C00266CAD52FB15D87C428579EA4D7D73A33669E12679E203329DD">>}, 11 | {<<"ranch">>, <<"F04166F456790FEE2AC1AA05A02745CC75783C2BFB26D39FAF6AEFC9A3D3A58A">>}]} 12 | ]. 13 | -------------------------------------------------------------------------------- /chapter_25/exercise_5/src/exercise_5.app.src: -------------------------------------------------------------------------------- 1 | {application, exercise_5, 2 | [{description, ""}, 3 | {vsn, "0.1"}, 4 | {registered, []}, 5 | {modules, [ 6 | cgi_web_server 7 | ]}, 8 | {applications, [ 9 | kernel 10 | ,stdlib 11 | ,cowboy 12 | ]}, 13 | {env, []} 14 | ]}. 15 | -------------------------------------------------------------------------------- /chapter_25/exercise_5/src/ping_pong.erl: -------------------------------------------------------------------------------- 1 | -module(ping_pong). 2 | 3 | -export([ping/1]). 4 | 5 | % Sample CGI function 6 | ping(_Arg) -> 7 | [<<"foobar">>]. 8 | -------------------------------------------------------------------------------- /chapter_25/exercise_5/src/rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /chapter_25/exercise_6/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /chapter_25/exercise_6/rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [cowboy, jsx]}. 2 | {escript_name, "exercise_6"}. 3 | {escript_incl_apps, [cowboy, jsx]}. 4 | -------------------------------------------------------------------------------- /chapter_25/exercise_6/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.4.0">>},0}, 3 | {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.3.0">>},1}, 4 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.9.0">>},0}, 5 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},1}]}. 6 | [ 7 | {pkg_hash,[ 8 | {<<"cowboy">>, <<"F1B72FABE9C8A5FC64AC5AC85FB65474D64733D1DF52A26FAD5D4BA3D9F70A9F">>}, 9 | {<<"cowlib">>, <<"BBD58EF537904E4F7C1DD62E6AA8BC831C8183CE4EFA9BD1150164FE15BE4CAA">>}, 10 | {<<"jsx">>, <<"D2F6E5F069C00266CAD52FB15D87C428579EA4D7D73A33669E12679E203329DD">>}, 11 | {<<"ranch">>, <<"F04166F456790FEE2AC1AA05A02745CC75783C2BFB26D39FAF6AEFC9A3D3A58A">>}]} 12 | ]. 13 | -------------------------------------------------------------------------------- /chapter_25/exercise_6/src/exercise_6.app.src: -------------------------------------------------------------------------------- 1 | {application, exercise_6, 2 | [{description, ""}, 3 | {vsn, "0.1"}, 4 | {registered, []}, 5 | {modules, [ 6 | cgi_web_server, 7 | exercise_6, 8 | ping_pong 9 | ]}, 10 | {applications, [ 11 | kernel 12 | ,stdlib 13 | ,cowboy 14 | ]}, 15 | {env, []}, 16 | {mod, {cgi_web_server, []}} 17 | ]}. 18 | -------------------------------------------------------------------------------- /chapter_25/exercise_6/src/exercise_6.erl: -------------------------------------------------------------------------------- 1 | -module(exercise_6). 2 | 3 | -export([main/1]). 4 | 5 | main(_Args) -> 6 | cgi_web_server:start(), 7 | timer:sleep(infinity). 8 | -------------------------------------------------------------------------------- /chapter_25/exercise_6/src/ping_pong.erl: -------------------------------------------------------------------------------- 1 | -module(ping_pong). 2 | 3 | -export([ping/1]). 4 | 5 | % Sample CGI function 6 | ping(_Arg) -> 7 | [<<"foobar">>]. 8 | -------------------------------------------------------------------------------- /chapter_25/exercise_6/src/rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /chapter_26/exercise_1/web_profiler.erl: -------------------------------------------------------------------------------- 1 | -module(web_profiler). 2 | 3 | -export([ping/2, receive_response/2]). 4 | 5 | ping(URL, Timeout) -> 6 | {_Protocol, Host, Port, Path} = parse_url(URL), 7 | {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {packet, 0}, {active, false}]), 8 | % Send the request 9 | ok = gen_tcp:send(Socket, io_lib:format("HEAD ~s HTTP/1.0\r\n\r\n", [Path])), 10 | % Time the response 11 | {Time, Result} = timer:tc(fun receive_response/2, [Socket, Timeout]), 12 | 13 | % Format the return value of the function 14 | case Result of 15 | timeout -> 16 | timeout; 17 | _ -> 18 | {time, Time} 19 | end. 20 | 21 | receive_response(Socket, Timeout) -> 22 | % And receive the response 23 | case gen_tcp:recv(Socket, 0, Timeout) of 24 | {ok, Packet} -> Packet; 25 | {error, timeout} -> timeout 26 | end. 27 | 28 | parse_url(Url) -> 29 | {ok, Parsed} = http_uri:parse(Url), 30 | % We ignore the query string for simplicity here 31 | {Protocol, _, Host, Port, Path, _Query} = Parsed, 32 | {Protocol, Host, Port, Path}. 33 | -------------------------------------------------------------------------------- /chapter_26/exercise_2/web_profiler.erl: -------------------------------------------------------------------------------- 1 | -module(web_profiler). 2 | 3 | -export([run/1, ping/2, receive_response/2]). 4 | 5 | -define(LIST_OF_WEBSITES, [ 6 | "http://stratus3d.com/", 7 | "http://lobste.rs/", 8 | "https://news.ycombinator.com/" 9 | ]). 10 | 11 | run(Timeout) -> 12 | % Map over the list 13 | {Time, _Result} = timer:tc(fun() -> 14 | lists:map(fun(Url) -> ping(Url, Timeout) end, ?LIST_OF_WEBSITES) 15 | end), 16 | 17 | % Return the time it took to execute all of the requests 18 | Time. 19 | 20 | ping(URL, Timeout) -> 21 | {_Protocol, Host, Port, Path} = parse_url(URL), 22 | {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {packet, 0}, {active, false}]), 23 | % Send the request 24 | ok = gen_tcp:send(Socket, io_lib:format("HEAD ~s HTTP/1.0\r\n\r\n", [Path])), 25 | % Time the response 26 | {Time, Result} = timer:tc(fun receive_response/2, [Socket, Timeout]), 27 | 28 | % Format the return value of the function 29 | case Result of 30 | timeout -> 31 | timeout; 32 | _ -> 33 | {time, Time} 34 | end. 35 | 36 | receive_response(Socket, Timeout) -> 37 | % And receive the response 38 | case gen_tcp:recv(Socket, 0, Timeout) of 39 | {ok, Packet} -> Packet; 40 | {error, timeout} -> timeout 41 | end. 42 | 43 | parse_url(Url) -> 44 | {ok, Parsed} = http_uri:parse(Url), 45 | % We ignore the query string for simplicity here 46 | {Protocol, _, Host, Port, Path, _Query} = Parsed, 47 | {Protocol, Host, Port, Path}. 48 | -------------------------------------------------------------------------------- /chapter_26/exercise_3/web_profiler.erl: -------------------------------------------------------------------------------- 1 | -module(web_profiler). 2 | 3 | -export([run/1, ping/2, receive_response/2]). 4 | 5 | -define(LIST_OF_WEBSITES, [ 6 | "http://stratus3d.com/", 7 | "http://lobste.rs/", 8 | "https://news.ycombinator.com/" 9 | ]). 10 | 11 | run(Timeout) -> 12 | % Map over the list 13 | {Time, _Result} = timer:tc(fun() -> 14 | pmap(fun(Url) -> ping(Url, Timeout) end, ?LIST_OF_WEBSITES) 15 | end), 16 | 17 | % Return the time it took to execute all of the requests 18 | Time. 19 | 20 | pmap(F, L) -> 21 | S = self(), 22 | Ref = make_ref(), 23 | Pids = lists:map(fun(I) -> 24 | spawn(fun() -> do_f(S, Ref, F, I) end) 25 | end, L), 26 | 27 | % Gather the results 28 | gather(Pids, Ref). 29 | 30 | do_f(Parent, Ref, F, I) -> 31 | Parent ! {self(), Ref, (catch F(I))}. 32 | 33 | gather([Pid|T], Ref) -> 34 | receive 35 | {Pid, Ref, Ret} -> [Ret|gather(T, Ref)] 36 | end; 37 | gather([], _) -> 38 | []. 39 | 40 | ping(URL, Timeout) -> 41 | {_Protocol, Host, Port, Path} = parse_url(URL), 42 | {ok, Socket} = gen_tcp:connect(Host, Port, [binary, {packet, 0}, {active, false}]), 43 | % Send the request 44 | ok = gen_tcp:send(Socket, io_lib:format("HEAD ~s HTTP/1.0\r\n\r\n", [Path])), 45 | % Time the response 46 | {Time, Result} = timer:tc(fun receive_response/2, [Socket, Timeout]), 47 | 48 | % Format the return value of the function 49 | case Result of 50 | timeout -> 51 | timeout; 52 | _ -> 53 | {time, Time} 54 | end. 55 | 56 | receive_response(Socket, Timeout) -> 57 | % And receive the response 58 | case gen_tcp:recv(Socket, 0, Timeout) of 59 | {ok, Packet} -> Packet; 60 | {error, timeout} -> timeout 61 | end. 62 | 63 | parse_url(Url) -> 64 | {ok, Parsed} = http_uri:parse(Url), 65 | % We ignore the query string for simplicity here 66 | {Protocol, _, Host, Port, Path, _Query} = Parsed, 67 | {Protocol, Host, Port, Path}. 68 | -------------------------------------------------------------------------------- /chapter_3/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 3 2 | 3 | ## 1. Memorize the commands on page 27. 4 | 5 | ## 2. Execute the `help()` function in the shell 6 | 7 | $ erl 8 | Erlang R16B01 (erts-5.10.2) [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] 9 | 10 | Eshell V5.10.2 (abort with ^G) 11 | 1> help(). 12 | ** shell internal commands ** 13 | b() -- display all variable bindings 14 | e(N) -- repeat the expression in query 15 | f() -- forget all variable bindings 16 | f(X) -- forget the binding of variable X 17 | h() -- history 18 | history(N) -- set how many previous commands to keep 19 | results(N) -- set how many previous command results to keep 20 | catch_exception(B) -- how exceptions are handled 21 | v(N) -- use the value of query 22 | rd(R,D) -- define a record 23 | rf() -- remove all record information 24 | rf(R) -- remove record information about R 25 | rl() -- display all record information 26 | rl(R) -- display record information about R 27 | rp(Term) -- display Term using the shell's record information 28 | rr(File) -- read record information from File (wildcards allowed) 29 | rr(F,R) -- read selected record information from file(s) 30 | rr(F,R,O) -- read selected record information with options 31 | ** commands in module c ** 32 | bt(Pid) -- stack backtrace for a process 33 | c(File) -- compile and load code in 34 | cd(Dir) -- change working directory 35 | flush() -- flush any messages sent to the shell 36 | help() -- help info 37 | i() -- information about the system 38 | ni() -- information about the networked system 39 | i(X,Y,Z) -- information about pid 40 | l(Module) -- load or reload module 41 | lc([File]) -- compile a list of Erlang modules 42 | ls() -- list files in the current directory 43 | ls(Dir) -- list files in directory 44 | m() -- which modules are loaded 45 | m(Mod) -- information about module 46 | memory() -- memory allocation information 47 | memory(T) -- memory allocation information of type 48 | nc(File) -- compile and load code in on all nodes 49 | nl(Module) -- load module on all nodes 50 | pid(X,Y,Z) -- convert X,Y,Z to a Pid 51 | pwd() -- print working directory 52 | q() -- quit - shorthand for init:stop() 53 | regs() -- information about registered processes 54 | nregs() -- information about all registered processes 55 | xm(M) -- cross reference check a module 56 | y(File) -- generate a Yecc parser 57 | ** commands in module i (interpreter interface) ** 58 | ih() -- print help for the i module 59 | true 60 | 2> 61 | 62 | ## 3. Try representing a house as a tuple and a street as a list of houses (tuples). 63 | 64 | So we are tasked with the job of representing a house as a tuple. A house could be represented many different ways in Erlang. Since the details are a little vague we will represent a house as a tuple of 4 elements taking this form: `{Address, NumberOfBedrooms, NumberOfBathrooms, ListOfAdditionalFeatures}`. Examples: 65 | 66 | {"1111 1st Street", 3, 2, [garage]} 67 | {"1113 1st Street", 3, 3, [pool, garage]} 68 | 69 | 70 | Now that we have defined the structure of the house tuples, we can create lists of tuples to represent streets: 71 | 72 | [{"1111 1st Street", 3, 2, [garage]}, 73 | {"1113 1st Street", 3, 3, [pool, garage]}, 74 | {"1116 1st Street", 3, 2, []}] 75 | 76 | -------------------------------------------------------------------------------- /chapter_4/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 4 2 | 3 | ## 1. Extend geometry.erl. Add clauses for the area of circles and right-angle triangles. Add clauses for computing the perimeter of shapes. 4 | 5 | Solution in the `exercise_1` directory. 6 | 7 | ## 2. Create your own implementation of `tuple_to_list/1` that doesn't use the BIF. 8 | 9 | Solution in the `exercise_2` directory. 10 | 11 | ## 3. Write a function that times execution of a function. Write a function that formats the time of day. 12 | 13 | Solution in the `exercise_3` directory. 14 | 15 | ## 4. Implement any of the functions from Python's datetime class that are not in Erlang's erlang module. 16 | 17 | Solution in the `exercise_4` directory. 18 | 19 | ## 5. Write a module with two functions - `even/1` and `odd/1`. They should return true or false based on whether the number is even or not. 20 | 21 | Solution in the `exercise_5` directory. 22 | 23 | ## 6. Implement the higher order function `filter/2`. The function should take a function that returns a boolean and a list. 24 | 25 | Solution in the `exercise_6` directory. 26 | 27 | ## 7. Write a function that uses the function from exercise #6 to split a list of numbers into two lists containing even and odd numbers. 28 | 29 | Solution in the `exercise_7` directory. 30 | -------------------------------------------------------------------------------- /chapter_4/exercise_1/geometry.erl: -------------------------------------------------------------------------------- 1 | -module(geometry). 2 | -export([area/1, perimeter/1]). 3 | 4 | area({rectangle, Width, Height}) -> Width * Height; 5 | area({square, Side}) -> Side * Side; 6 | %% exercise called for additional clauses to compute the 7 | %% area of circles and triangles. 8 | area({circle, Radius}) -> (Radius * Radius) * math:pi(); 9 | area({triangle, Width, Height}) -> (Width * Height) / 2. 10 | 11 | perimeter({rectangle, Width, Height}) -> Width * Height * 2; 12 | perimeter({square, Side}) -> Side * Side * 2; 13 | perimeter({circle, Radius}) -> (Radius * 2) * math:pi(); 14 | perimeter({triangle, Width, Height}) -> math:sqrt((Width * Width) + (Height * Height)). 15 | -------------------------------------------------------------------------------- /chapter_4/exercise_2/my_tuple_to_list.erl: -------------------------------------------------------------------------------- 1 | -module(my_tuple_to_list). 2 | 3 | -export([my_tuple_to_list/1]). 4 | 5 | % Convert a tuple to a list without using the BIF 6 | my_tuple_to_list(Tuple) -> 7 | Size = size(Tuple), 8 | % Get the size of the tuple so we know how many items 9 | % to copy over to the list. 10 | do_tuple_to_list(Size, Tuple, []). 11 | 12 | do_tuple_to_list(0, _Tuple, List) -> 13 | % Once there are no remaining items in the 14 | % tuple return the list 15 | List; 16 | 17 | do_tuple_to_list(Remaining, Tuple, List) -> 18 | % Take the last item of the tuple and added it to 19 | % the beginning of the list (list is created in 20 | % reverse order). 21 | NewItem = element(Remaining, Tuple), 22 | do_tuple_to_list(Remaining - 1, Tuple, [NewItem|List]). 23 | -------------------------------------------------------------------------------- /chapter_4/exercise_3/my_time_and_date.erl: -------------------------------------------------------------------------------- 1 | -module(my_time_and_date). 2 | 3 | -export([my_time_func/1, my_date_string/0]). 4 | 5 | my_time_func(F) -> 6 | % Get the microseconds before invoking the function 7 | {_, _, StartMicro} = now(), 8 | % Invoke the function 9 | F(), 10 | % Get the microseconds again 11 | {_, _, FinishMicro} = now(), 12 | % Compute the number of microseconds it took for the 13 | % function to return 14 | FinishMicro - StartMicro. 15 | 16 | my_date_string() -> 17 | % Get the year month and day from the date/0 function 18 | {Year, Month, Day} = date(), 19 | % Get the hour minute and second from the time/0 function 20 | {Hour, Minute, Second} = time(), 21 | % Use io_lib to format the values into a readable date time 22 | TimeList = io_lib:format( 23 | "~.2.0w/~.2.0w/~.4.0w ~.2.0w:~.2.0w:~.2.0w", 24 | [Month, Day, Year, Hour, Minute, Second]), 25 | % Flatten the TimeList as it contains nested lists (nested 26 | % lists prevent the list from being formatted as a string). 27 | lists:flatten(TimeList). 28 | -------------------------------------------------------------------------------- /chapter_4/exercise_4/python_datetime.erl: -------------------------------------------------------------------------------- 1 | -module(python_datetime). 2 | 3 | % Since I am not really familiar with Python's datetime class 4 | % I had to spend some time browsing the documentation to figure 5 | % out what Erlang lacked. 6 | % 7 | % Python's datetime methods and Erlang's equivalent function: 8 | % Python Erlang 9 | % __add__ None 10 | % __eq__ == 11 | % __ge__ >= 12 | % __getattribute__ Not needed 13 | % __gt__ > 14 | % __hash__ Not needed 15 | % __le__ =< 16 | % __lt__ < 17 | % __ne__ /= 18 | % __radd__ Not needed 19 | % __repr__ Not needed 20 | % __rsub__ Not needed 21 | % __str__ Not needed 22 | % __sub__ - 23 | % 24 | % All the other datetime methods are provided by Erlang functions in the erlang 25 | % and calendar modules. 26 | -------------------------------------------------------------------------------- /chapter_4/exercise_5/math_functions.erl: -------------------------------------------------------------------------------- 1 | -module(math_functions). 2 | 3 | -export([even/1, odd/1]). 4 | 5 | even(Number) -> 6 | % Use `rem` to check if the remainder is 0 7 | % if it is return true, otherwise false 8 | 0 =:= Number rem 2. 9 | 10 | odd(Number) -> 11 | % Use `rem` to check if the remainder is 1 12 | % if it is return true, otherwise false 13 | % (for odd numbers the remainder of 14 | % division by 2 will always be 1) 15 | 1 =:= Number rem 2. 16 | -------------------------------------------------------------------------------- /chapter_4/exercise_6/math_functions.erl: -------------------------------------------------------------------------------- 1 | -module(math_functions). 2 | 3 | -export([even/1, odd/1, filter/2]). 4 | 5 | even(Number) -> 6 | % Use `rem` to check if the remainder is 0 7 | % if it is return true, otherwise false 8 | 0 =:= Number rem 2. 9 | 10 | odd(Number) -> 11 | % Use `rem` to check if the remainder is 1 12 | % if it is return true, otherwise false 13 | % (for odd numbers the remainder of 14 | % division by 2 will always be 1) 15 | 1 =:= Number rem 2. 16 | 17 | filter(Fun, List) -> 18 | % Invoke filter/3, which does the real work 19 | % then reverse the resulting list, as 20 | % filter/3 returns a list in reverse order. 21 | lists:reverse(filter(Fun, List, [])). 22 | 23 | filter(_Fun, [], Result) -> 24 | % If there are no more items in the list 25 | % return the result 26 | Result; 27 | filter(Fun, [Item|Remaining], Result) -> 28 | % If another item still exists in the list 29 | % Apply `Fun` to it and check the result, 30 | % if true, add the item to the result. 31 | case Fun(Item) of 32 | true -> 33 | filter(Fun, Remaining, [Item|Result]); 34 | _ -> 35 | filter(Fun, Remaining, Result) 36 | end. 37 | -------------------------------------------------------------------------------- /chapter_4/exercise_7/math_functions.erl: -------------------------------------------------------------------------------- 1 | -module(math_functions). 2 | 3 | -export([even/1, odd/1, filter/2, split1/1, split2/1]). 4 | 5 | even(Number) -> 6 | % Use `rem` to check if the remainder is 0 7 | % if it is return true, otherwise false 8 | 0 =:= Number rem 2. 9 | 10 | odd(Number) -> 11 | % Use `rem` to check if the remainder is 1 12 | % if it is return true, otherwise false 13 | % (for odd numbers the remainder of 14 | % division by 2 will always be 1) 15 | 1 =:= Number rem 2. 16 | 17 | filter(Fun, List) -> 18 | % Invoke filter/3, which does the real work 19 | % then reverse the resulting list, as 20 | % filter/3 returns a list in reverse order. 21 | lists:reverse(filter(Fun, List, [])). 22 | 23 | filter(_Fun, [], Result) -> 24 | % If there are no more items in the list 25 | % return the result 26 | Result; 27 | filter(Fun, [Item|Remaining], Result) -> 28 | % If another item still exists in the list 29 | % Apply `Fun` to it and check the result, 30 | % if true, add the item to the result. 31 | case Fun(Item) of 32 | true -> 33 | filter(Fun, Remaining, [Item|Result]); 34 | _ -> 35 | filter(Fun, Remaining, Result) 36 | end. 37 | 38 | split1(List) -> 39 | % For the first version of split we use an 40 | % accumulator. So we defined split/2 to pass 41 | % along the accumulator to each recursive call 42 | split(List, {[], []}). 43 | 44 | split([], {Even, Odd}) -> 45 | % The Even and Odd lists were constructed in 46 | % reverse so we reverse them to correct the 47 | % order. 48 | {lists:reverse(Even), lists:reverse(Odd)}; 49 | split([Item|List], {Even, Odd}) -> 50 | % In order to determine what list an item 51 | % should be added to we pass it to even/1. If 52 | % it returns true we add it to the Even list, 53 | % otherwise we add it to the Odd list. 54 | case ?MODULE:even(Item) of 55 | true -> 56 | split(List, {[Item|Even], Odd}); 57 | false -> 58 | split(List, {Even, [Item|Odd]}) 59 | end. 60 | 61 | split2(List) -> 62 | % In the second version of the split function 63 | % we simply invoke the filter/2 function and 64 | % pass in a reference to the even/1 or odd/1 65 | % functions. The first call returns all the even 66 | % items and the second returns all the odd items. 67 | Even = filter(fun even/1, List), 68 | Odd = filter(fun odd/1, List), 69 | % Then we simply return both lists 70 | {Even, Odd}. 71 | -------------------------------------------------------------------------------- /chapter_5/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 5 2 | 3 | ## 1. Write some functions to read configuration files containing JSON and turn them into Erlang maps. Also write code to validate the data in the configuration files. 4 | Unlike what's shown in the book, the map module doesn't actually have a `to_json/1` and `from_json/1` functions. jsx is a JSON library that has similar functions, so we will use it instead. Solution to the exercise is in the `exercise_1` directory. Example usage: 5 | 6 | 1> json_configuration:config_to_map("sample_config.json"). 7 | [{<<"param1">>,<<"value1">>}, 8 | {<<"param2">>,<<"value2">>}, 9 | {<<"param3">>,[<<"one">>,<<"two">>,<<"three">>]}, 10 | {<<"param4">>,[]}, 11 | {<<"param5">>, 12 | [{<<"sub1">>,<<"test1">>},{<<"sub2">>,<<"test2">>}]}] 13 | 2> json_configuration:verify_config("sample_config.json"). 14 | true 15 | 16 | ## 2. Write a function named `map_search_pred` that takes a map `Map` and a predicate `Pred` that returns the first element which `Pred` returns true. 17 | Solution to the exercise is in the `exercise_2` directory. Example usage: 18 | 19 | % Function that returns true when the key is `baz` 20 | 1> Fun = fun(Key, Val) -> 21 | 1> case Key of 22 | 1> baz -> 23 | 1> true; 24 | 1> _ -> 25 | 1> false 26 | 1> end 27 | 1> end. 28 | #Fun 29 | 30 | % Passing a map and the predicate function to `map_search_pred` 31 | 2> map_search:map_search_pred(#{first => 'Joe', last => 'Armstrong'}, Fun). 32 | {baz,bim} 33 | 3> 34 | 35 | ## 3. Make an Erlang module with functions equivalent to the methods defined in Ruby's Hash class. 36 | Solution to the exercise is in the `exercise_3` directory. 37 | -------------------------------------------------------------------------------- /chapter_5/exercise_1/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean console rebar deps get_deps 2 | 3 | build: 4 | erlc *.erl 5 | 6 | clean: 7 | rm -rf deps 8 | 9 | console: 10 | erl -pa ebin/ -pa deps/jsx/ebin/ 11 | 12 | deps: 13 | git clone git@github.com:talentdeficit/jsx.git deps/jsx/ 14 | cd deps/jsx && make 15 | -------------------------------------------------------------------------------- /chapter_5/exercise_1/json_configuration.erl: -------------------------------------------------------------------------------- 1 | -module(json_configuration). 2 | 3 | -export([config_to_map/1, verify_config/1]). 4 | 5 | config_to_map(Filename) -> 6 | case file:read_file(Filename) of 7 | {ok, Binary} -> 8 | jsx:decode(Binary); 9 | Error -> 10 | Error 11 | end. 12 | 13 | verify_config(Filename) -> 14 | Config = config_to_map(Filename), 15 | case is_list(Config) of 16 | true -> 17 | is_proplist(Config); 18 | false -> 19 | false 20 | end. 21 | 22 | % Private functions 23 | is_proplist([]) -> 24 | true; 25 | is_proplist([{_Key, _Value}|List]) -> 26 | is_proplist(List); 27 | is_proplist([_|_List]) -> 28 | false. 29 | -------------------------------------------------------------------------------- /chapter_5/exercise_1/sample_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "param1": "value1", 3 | "param2": "value2", 4 | "param3": ["one", "two", "three"], 5 | "param4": [], 6 | "param5": { 7 | "sub1": "test1", 8 | "sub2": "test2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /chapter_5/exercise_2/map_search.erl: -------------------------------------------------------------------------------- 1 | -module(map_search). 2 | 3 | -export([map_search_pred/2]). 4 | 5 | map_search_pred(Map, Pred) -> 6 | % We need to convert the map to a list before we can recurse over it 7 | Proplist = maps:to_list(Map), 8 | % Once we have a proplist we can recurse over it and return the key/value 9 | % pair matching the `Pred` function. 10 | proplist_search_pred(Proplist, Pred). 11 | 12 | proplist_search_pred([], _Pred) -> 13 | % If no matching key/value pairs are found return false 14 | false; 15 | proplist_search_pred([{Key, Value}|Map], Pred) -> 16 | case Pred(Key, Value) of 17 | true -> 18 | % If the predicate function returns true return the key/value pair 19 | {Key, Value}; 20 | false -> 21 | % Otherwise continue to search for a matching pair 22 | proplist_search_pred(Map, Pred) 23 | end. 24 | -------------------------------------------------------------------------------- /chapter_5/exercise_3/hash.erl: -------------------------------------------------------------------------------- 1 | -module(hash). 2 | 3 | % Our own version of Ruby's Hash class 4 | -export([ 5 | any/2, 6 | all/2, 7 | delete/2, 8 | dig/2, 9 | empty/1, 10 | has_key/2, 11 | has_value/2, 12 | keys/1, 13 | values/1, 14 | merge/2, 15 | reject/2, 16 | select/2, 17 | shift/1, 18 | size/1, 19 | store/3 20 | ]). 21 | 22 | % Return true if `Predicate` returns true for any of the pairs in the map 23 | any(Hash, Predicate) -> 24 | Proplist = maps:to_list(Hash), 25 | lists:any(Predicate, Proplist). 26 | 27 | % Return true if `Predicate` returns true for all of the pairs in the map 28 | all(Hash, Predicate) -> 29 | Proplist = maps:to_list(Hash), 30 | lists:all(Predicate, Proplist). 31 | 32 | % Delete a pair from the map and return the new map 33 | delete(Hash, Key) -> 34 | maps:remove(Key, Hash). 35 | 36 | % Retrieve a value from a nested map 37 | dig(Hash, Keys) -> 38 | do_dig(Hash, Keys). 39 | 40 | % Returns whether or not the map is empty 41 | empty(Hash) -> 42 | maps:size(Hash) > 0. 43 | 44 | % Returns whether or not `Key` is present as a key in map `Hash` 45 | has_key(Hash, Key) -> 46 | lists:member(Key, keys(Hash)). 47 | 48 | % Returns whether or not `Value` is present as a value in map `Hash` 49 | has_value(Hash, Value) -> 50 | lists:member(Value, values(Hash)). 51 | 52 | % Returns all the keys in the map 53 | keys(Hash) -> 54 | maps:keys(Hash). 55 | 56 | % Returns all the values in the map 57 | values(Hash) -> 58 | maps:values(Hash). 59 | 60 | % Merges two maps and returns the resulting map 61 | merge(Hash1, Hash2) -> 62 | maps:merge(Hash1, Hash2). 63 | 64 | % Returns a map containing all the pairs for which `Predicate` returned false 65 | reject(Hash, Predicate) -> 66 | maps:filter(fun(Key, Value) -> 67 | Predicate(Key, Value) =:= false 68 | end, Hash). 69 | 70 | % Returns a map containing all the pairs for which `Predicate` returned true 71 | select(Hash, Predicate) -> 72 | maps:filter(fun(Key, Value) -> 73 | Predicate(Key, Value) =:= true 74 | end, Hash). 75 | 76 | % Removes a key from the map and returns the new map 77 | shift(Hash) -> 78 | [Key|_] = keys(Hash), 79 | maps:remove(Key, Hash). 80 | 81 | % Returns the size of the map 82 | size(Hash) -> 83 | maps:size(Hash). 84 | 85 | % Stores a new key/value pair in the map and returns a new map 86 | store(Hash, Key, Value) -> 87 | maps:put(Hash, Key, Value). 88 | 89 | % Private functions 90 | do_dig(Value, []) -> 91 | Value; 92 | do_dig(Value, [Key|Keys]) -> 93 | case is_map(Value) of 94 | true -> % Retrieve a value from a nested map 95 | NewValue = maps:get(Key, Value), 96 | do_dig(NewValue, Keys); 97 | false -> % Add support for proplists too 98 | proplists:get_value(Key, Value) 99 | end. 100 | -------------------------------------------------------------------------------- /chapter_6/exercise_1/myfile.erl: -------------------------------------------------------------------------------- 1 | -module(myfile). 2 | 3 | -export([read/1]). 4 | 5 | % myfile:read returns the contents of the file at 6 | % the specified path or raise an exception if the file 7 | % doesn't exist or an error occurs while reading. 8 | % 9 | % While the exercise doesn't specify it, I am assuming 10 | % that we want to control the exception that is raised 11 | % so I am using a case statement. 12 | read(File) -> 13 | case file:read_file(File) of 14 | {ok, Bin} -> 15 | % Read was successful, return contents of file. 16 | Bin; 17 | {error, Why} -> 18 | % Raise an exception with `{failed, Why}` reason. 19 | throw({failed, Why}) 20 | end. 21 | -------------------------------------------------------------------------------- /chapter_6/exercise_2/try_test.erl: -------------------------------------------------------------------------------- 1 | -module(try_test). 2 | 3 | -export([generate_exception/1, demo1/0, demo2/0, demo3/0, polite_and_detailed_messages/0]). 4 | 5 | generate_exception(1) -> a; 6 | generate_exception(2) -> throw(a); 7 | generate_exception(3) -> exit(a); 8 | generate_exception(4) -> {'EXIT', a}; 9 | generate_exception(5) -> error(a). 10 | 11 | demo1() -> 12 | [catcher(I) || I <- [1,2,3,4,5]]. 13 | 14 | catcher(N) -> 15 | try generate_exception(N) of 16 | Val -> {N, normal, Val} 17 | catch 18 | throw:X -> {N, caught, throw, X}; 19 | exit:X -> {N, caught, exited, X}; 20 | error:X -> {N, caught, error, X} 21 | end. 22 | 23 | demo2() -> 24 | [{I, (catch generate_exception(I))} || I <- [1,2,3,4,5]]. 25 | 26 | demo3() -> 27 | try generate_exception(5) 28 | catch 29 | error:X -> 30 | {X, erlang:get_stacktrace()} 31 | end. 32 | 33 | % Exercise 6.2 says we need to provide two messages for each 34 | % exception. To do that I duplicated the catcher function above 35 | % and updated the return value so that is it always two tuples. 36 | % The first item in the tuple is a tuple representing the polite 37 | % error message for the user. The second is a tuple of four items 38 | % for the developer. The last item in the tuple is the stacktrace. 39 | polite_and_detailed_message(N) -> 40 | try generate_exception(N) of 41 | Val -> {Val, {N, normal, Val, erlang:get_stacktrace()}} 42 | catch 43 | throw:X -> {{caught_throw, X}, {N, caught, throw, X, erlang:get_stacktrace()}}; 44 | exit:X -> {{caught_exit, X}, {N, caught, exited, X, erlang:get_stacktrace()}}; 45 | error:X -> {{caught_error, X}, {N, caught, error, X, erlang:get_stacktrace()}} 46 | end. 47 | 48 | % In order to demonstrate the polite_and_detailed_message 49 | % function, I created this function called 50 | % polite_and_detailed_messages. It is almost the same as 51 | % demo1/0. 52 | polite_and_detailed_messages() -> 53 | [polite_and_detailed_message(I) || I <- [1,2,3,4,5]]. 54 | -------------------------------------------------------------------------------- /chapter_7/exercise_1/reverse_bytes.erl: -------------------------------------------------------------------------------- 1 | -module(reverse_bytes). 2 | 3 | -export([reverse/1]). 4 | 5 | % We take a binary and then need to reverse all the bytes. Reversing lists is 6 | % very efficient (and very easy) in Erlang. So we convert the binary to a list, 7 | % reverse the list, and the conver the list back to a binary. 8 | reverse(Binary) when is_binary(Binary) -> 9 | list_to_binary(lists:reverse(binary_to_list(Binary))). 10 | -------------------------------------------------------------------------------- /chapter_7/exercise_2/term_to_packet.erl: -------------------------------------------------------------------------------- 1 | -module(term_to_packet). 2 | 3 | -export([term_to_packet/1]). 4 | 5 | -define(LENGTH_HEADER, 32). % 8 (byte) * 4 6 | 7 | term_to_packet(Term) -> 8 | BinTerm = term_to_binary(Term), % The payload 9 | Length = byte_size(BinTerm), % The value for the 4 byte header 10 | % Then we construct the binary. First we set the length as the first 4 bytes. 11 | % Then the BinTerm binary with the payload. 12 | <>. 13 | -------------------------------------------------------------------------------- /chapter_7/exercise_3/packet_to_term.erl: -------------------------------------------------------------------------------- 1 | -module(packet_to_term). 2 | 3 | -export([packet_to_term/1]). 4 | 5 | -define(LENGTH_HEADER, 32). % 8 (byte) * 4 6 | 7 | packet_to_term(Packet) -> 8 | % Extract the length from the head of the binary 9 | <> = Packet, 10 | % Take the packet payload portion of the binary and convert it back to an 11 | % Erlang term 12 | binary_to_term(BinTerm). 13 | -------------------------------------------------------------------------------- /chapter_7/exercise_4/packet_conversions.erl: -------------------------------------------------------------------------------- 1 | -module(packet_conversions). 2 | 3 | -export([test/0]). 4 | 5 | -define(LENGTH_HEADER, 32). % 8 (byte) * 4 6 | 7 | test() -> 8 | % Test conversion from term to packet 9 | Term = {foo, bar, baz}, 10 | ExpectedLength = byte_size(term_to_binary(Term)), 11 | ExpectedTotalLength = ExpectedLength + 4, 12 | 13 | % Assert that the binary returned is in the format we expect 14 | Packet = <> = term_to_packet:term_to_packet(Term), 15 | 16 | % Assert the binary is the right size 17 | ExpectedLength = Length, 18 | ExpectedTotalLength = byte_size(Packet), 19 | Term = binary_to_term(Payload), 20 | 21 | % Test conversion from packet to term 22 | Term = packet_to_term:packet_to_term(Packet), 23 | ok. 24 | -------------------------------------------------------------------------------- /chapter_7/exercise_5/reverse_bits.erl: -------------------------------------------------------------------------------- 1 | -module(reverse_bits). 2 | 3 | -export([reverse/1]). 4 | 5 | % We take a binary and then need to reverse all the bits. 6 | reverse(Binary) -> 7 | % Call the recursive function reverse/2 with the binary and an empty binary 8 | % as the accumlator. 9 | reverse(Binary, <<>>). 10 | 11 | reverse(<<>>, Result) -> 12 | % Reverse the binary until we get to the end, then return the accumlator 13 | Result; 14 | reverse(<>, Result) -> 15 | % Take the first bit off the front of the binary and push it onto the 16 | % accumlator binary. Call the function again with the remaining bits 17 | reverse(Bin, <>). 18 | -------------------------------------------------------------------------------- /chapter_8/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 8 2 | 3 | ## 1. Read the section about `Mod:module_info()`. How many functions does `dict` have? 4 | In `exercise_1/` there is a module named `dict_functions` that contains a `count` function. The function will print the number of exported functions from the `dict` module. 5 | 6 | ## 2. Write a function to list all loaded modules. Write a function that finds the module with the most exported functions and one that finds the most common function name. Write another function to list unambiguous function names. 7 | In `exercise_2/` there is a module named `module_functions` that contains a `print_export_details` function. The function will print a list of all loaded modules, the module with the most exported functions, the most common function name, and a list of all unambiguous function names. 8 | 9 | -------------------------------------------------------------------------------- /chapter_8/exercise_1/dict_functions.erl: -------------------------------------------------------------------------------- 1 | -module(dict_functions). 2 | 3 | -export([count/0]). 4 | 5 | % count/0 prints the count of the functions exported from `dict` to STDOUT 6 | count() -> 7 | % module_info(exports) returns a list of all exported functions 8 | Functions = dict:module_info(exports), 9 | 10 | % We take the length of the list that contains the exported functions 11 | NumFunctions = length(Functions), 12 | 13 | % And then we print the number of exported functions 14 | io:fwrite("Dict exports ~w functions.\n", [NumFunctions]), 15 | ok. 16 | -------------------------------------------------------------------------------- /chapter_9/README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Chapter 9 2 | 3 | ## 1. Write a small module. Write type specs for the exported functions. Make a type error in one of the functions and run dialyzer against the module. 4 | Code for this exercise is stored in `exercise_1/`. The command I used to run dialyzer was `dialyzer simple_types.erl`. You will also need to have a PLT file generated. 5 | 6 | ## 2. Find the type annotations in the standard libraries. Read the type annotations in the `lists` module. 7 | 8 | ## 3. Why is it a good idea to think about types before you write code? 9 | 10 | Types are part of your API. The functions you define will only accept certain types, so it's important to figure out what types of data your API should be handling. This doesn't mean you always must have valid type specs during development though, once the types have been thought through it's fine to comment them out during a "cowboy coding" session and uncomment them when you are finishing with the function or module you are developing. 11 | 12 | ## 4. Create a module that exports an opaque type. Then create a second module that uses the type in such a way as to cause an abstraction violation. 13 | Code for this exercise is stored in `exercise_4/`. 14 | -------------------------------------------------------------------------------- /chapter_9/exercise_1/simple_types.erl: -------------------------------------------------------------------------------- 1 | -module(simple_types). 2 | 3 | -export([add/2, concat/2]). 4 | 5 | % Function spec that is correct 6 | -spec add(X :: number(), Y :: number()) -> number(). 7 | 8 | add(X, Y) -> 9 | X + Y. 10 | 11 | % Function spec with incorrect function argument types 12 | -spec concat(String1 :: atom(), String2 :: string()) -> list(). 13 | 14 | concat(String1, String2) when is_integer(String1) -> 15 | [String1] ++ String2; 16 | 17 | concat(String1, String2) when is_integer(String2) -> 18 | String1 ++ [String2]; 19 | 20 | concat(String1, String2) when is_integer(String1), is_integer(String2) -> 21 | [String1, String2]. 22 | -------------------------------------------------------------------------------- /chapter_9/exercise_4/opaque_record.erl: -------------------------------------------------------------------------------- 1 | -module(opaque_record). 2 | 3 | -record(user, {name, email, password}). 4 | 5 | % Opaque type 6 | -opaque user() :: #user{}. 7 | 8 | -export_type([user/0]). 9 | 10 | -export([new_user/0]). 11 | 12 | % Function that returns value that is of the opaque type 13 | -spec new_user() -> user(). 14 | 15 | new_user() -> 16 | #user{}. 17 | -------------------------------------------------------------------------------- /chapter_9/exercise_4/opaque_violation.erl: -------------------------------------------------------------------------------- 1 | -module(opaque_violation). 2 | 3 | -export([test/0]). 4 | 5 | test() -> 6 | % Function that returns a value that is of an opaque type 7 | User = opaque_record:new_user(), 8 | 9 | % Violate opaqueness by treating the User variable, which is the opaque 10 | % type, as a tuple. 11 | {user, Name, _Email, _Password} = User, 12 | Name. 13 | --------------------------------------------------------------------------------