From 09665c3b885fb7643b79f4214129d3d907a52a6b Mon Sep 17 00:00:00 2001 From: AYM1607 Date: Sun, 21 Jun 2020 11:02:00 -0500 Subject: [PATCH] Create: Finished code exercises for the 6th chapter --- chapter6/key_value_gen_server.ex | 38 ++++++ chapter6/key_value_gen_server_registered.ex | 40 ++++++ chapter6/server_process_cast.ex | 68 ++++++++++ chapter6/server_process_todo.ex | 133 ++++++++++++++++++++ chapter6/todo_server.ex | 96 ++++++++++++++ 5 files changed, 375 insertions(+) create mode 100644 chapter6/key_value_gen_server.ex create mode 100644 chapter6/key_value_gen_server_registered.ex create mode 100644 chapter6/server_process_cast.ex create mode 100644 chapter6/server_process_todo.ex create mode 100644 chapter6/todo_server.ex diff --git a/chapter6/key_value_gen_server.ex b/chapter6/key_value_gen_server.ex new file mode 100644 index 0000000..f19db5b --- /dev/null +++ b/chapter6/key_value_gen_server.ex @@ -0,0 +1,38 @@ +defmodule KeyValueStore do + use GenServer + + def start do + GenServer.start(KeyValueStore, nil) + end + + def put(pid, key, value) do + GenServer.cast(pid, {:put, key, value}) + end + + def get(pid, key) do + GenServer.call(pid, {:get, key}) + end + + @impl true + def init(_) do + # Set up the process to send itself a cleanup message every 5 secs. + :timer.send_interval(5000, :cleanup) + {:ok, %{}} + end + + @impl true + def handle_cast({:put, key, value}, state) do + {:noreply, Map.put(state, key, value)} + end + + @impl true + def handle_call({:get, key}, state) do + {:reply, Map.get(state, key), state} + end + + @impl true + def handle_info(:cleanup, state) do + IO.puts("Performing cleanup...") + {:noreply, state} + end +end diff --git a/chapter6/key_value_gen_server_registered.ex b/chapter6/key_value_gen_server_registered.ex new file mode 100644 index 0000000..dd41a34 --- /dev/null +++ b/chapter6/key_value_gen_server_registered.ex @@ -0,0 +1,40 @@ +defmodule KeyValueStore do + use GenServer + + def start do + # At compile time, __MODULE__ is replaced with KeyValueStore + # This makes refactoring easier. + GenServer.start(__MODULE__, nil, name: __MODULE__) + end + + def put(key, value) do + GenServer.cast(__MODULE__, {:put, key, value}) + end + + def get(key) do + GenServer.call(__MODULE__, {:get, key}) + end + + @impl true + def init(_) do + # Set up the process to send itself a cleanup message every 5 secs. + :timer.send_interval(5000, :cleanup) + {:ok, %{}} + end + + @impl true + def handle_cast({:put, key, value}, state) do + {:noreply, Map.put(state, key, value)} + end + + @impl true + def handle_call({:get, key}, _, state) do + {:reply, Map.get(state, key), state} + end + + @impl true + def handle_info(:cleanup, state) do + IO.puts("Performing cleanup...") + {:noreply, state} + end +end diff --git a/chapter6/server_process_cast.ex b/chapter6/server_process_cast.ex new file mode 100644 index 0000000..721f08d --- /dev/null +++ b/chapter6/server_process_cast.ex @@ -0,0 +1,68 @@ +defmodule ServerProcess do + def start(callback_module) do + spawn(fn -> + initial_state = callback_module.init() + loop(callback_module, initial_state) + end) + end + + def loop(callback_module, current_state) do + receive do + {:call, request, caller} -> + {response, new_state} = + callback_module.handle_call( + request, + current_state + ) + + send(caller, {:response, response}) + loop(callback_module, new_state) + + {:cast, request} -> + new_state = + callback_module.handle_cast( + request, + current_state + ) + + loop(callback_module, new_state) + end + end + + def call(server_pid, request) do + send(server_pid, {:call, request, self()}) + + receive do + {:response, response} -> + response + end + end + + def cast(server_pid, request) do + send(server_pid, {:cast, request}) + end +end + +defmodule KeyValueStore do + def init, do: %{} + + def start do + ServerProcess.start(KeyValueStore) + end + + def put(pid, key, value) do + ServerProcess.cast(pid, {:put, key, value}) + end + + def get(pid, key) do + ServerProcess.call(pid, {:get, key}) + end + + def handle_call({:get, key}, state) do + {Map.get(state, key), state} + end + + def handle_cast({:put, key, value}, state) do + Map.put(state, key, value) + end +end diff --git a/chapter6/server_process_todo.ex b/chapter6/server_process_todo.ex new file mode 100644 index 0000000..2bdb3a8 --- /dev/null +++ b/chapter6/server_process_todo.ex @@ -0,0 +1,133 @@ +defmodule ServerProcess do + def start(callback_module) do + spawn(fn -> + initial_state = callback_module.init() + loop(callback_module, initial_state) + end) + end + + def loop(callback_module, current_state) do + receive do + {:call, request, caller} -> + {response, new_state} = + callback_module.handle_call( + request, + current_state + ) + + send(caller, {:response, response}) + loop(callback_module, new_state) + + {:cast, request} -> + new_state = + callback_module.handle_cast( + request, + current_state + ) + + loop(callback_module, new_state) + end + end + + def call(server_pid, request) do + send(server_pid, {:call, request, self()}) + + receive do + {:response, response} -> + response + end + end + + def cast(server_pid, request) do + send(server_pid, {:cast, request}) + end +end + +defmodule TodoServer do + def start do + ServerProcess.start(TodoServer) + end + + def init, do: TodoList.new() + + def entries(server_pid, date) do + ServerProcess.call(server_pid, {:entries, date}) + end + + def add_entry(server_pid, entry) do + ServerProcess.cast(server_pid, {:add_entry, entry}) + end + + def delete_entry(server_pid, entry_id) do + ServerProcess.cast(server_pid, {:delete_entry, entry_id}) + end + + def update_entry(server_pid, entry_id, updater_fun) do + ServerProcess.cast(server_pid, {:update_entry, entry_id, updater_fun}) + end + + def handle_cast({:add_entry, entry}, state) do + TodoList.add_entry(state, entry) + end + + def handle_cast({:delete_entry, entry_id}, state) do + TodoList.delete_entry(state, entry_id) + end + + def handle_cast({:update_entry, entry_id, updater_fun}, state) do + TodoList.update_entry(state, entry_id, updater_fun) + end + + def handle_call({:entries, date}, state) do + entries = TodoList.entries(state, date) + {entries, state} + end +end + +defmodule TodoList do + defstruct auto_id: 1, entries: %{} + + def new(), do: %TodoList{} + + def add_entry(todo_list, entry) do + entry = Map.put(entry, :id, todo_list.auto_id) + + new_entries = + Map.put( + todo_list.entries, + todo_list.auto_id, + entry + ) + + %TodoList{todo_list | entries: new_entries, auto_id: todo_list.auto_id + 1} + end + + def entries(todo_list, date) do + todo_list.entries + |> Stream.filter(fn {_, entry} -> entry.date == date end) + |> Enum.map(fn {_, entry} -> entry end) + end + + def update_entry(todo_list, %{} = new_entry) do + update_entry(todo_list, new_entry.id, fn _ -> new_entry end) + end + + def update_entry(todo_list, entry_id, updater_fun) do + case Map.fetch(todo_list.entries, entry_id) do + :error -> + todo_list + + {:ok, old_entry} -> + old_entry_id = old_entry.id + # Make sure that the result of the updater is a map and the + # id remains unchanged. + new_entry = %{id: ^old_entry_id} = updater_fun.(old_entry) + new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) + %TodoList{todo_list | entries: new_entries} + end + end + + def delete_entry(todo_list, entry_id) do + %TodoList{todo_list | entries: Map.delete(todo_list.entries, entry_id)} + end +end diff --git a/chapter6/todo_server.ex b/chapter6/todo_server.ex new file mode 100644 index 0000000..08d111f --- /dev/null +++ b/chapter6/todo_server.ex @@ -0,0 +1,96 @@ +defmodule TodoServer do + use GenServer + + def start do + GenServer.start(__MODULE__, nil) + end + + def add_entry(pid, entry) do + GenServer.cast(pid, {:add_entry, entry}) + end + + def delete_entry(pid, entry_id) do + GenServer.cast(pid, {:delete_entry, entry_id}) + end + + def update_entry(pid, entry_id, updater_fun) do + GenServer.cast(pid, {:update_entry, entry_id, updater_fun}) + end + + def entries(pid, date) do + GenServer.call(pid, {:entries, date}) + end + + @impl true + def init(_) do + {:ok, TodoList.new()} + end + + @impl true + def handle_cast({:add_entry, entry}, state) do + {:noreply, TodoList.add_entry(state, entry)} + end + + @impl true + def handle_cast({:delete_entry, entry_id}, state) do + {:noreply, TodoList.delete_entry(state, entry_id)} + end + + @impl true + def handle_cast({:update_entry, entry_id, updater_fun}, state) do + {:noreply, TodoList.update_entry(state, entry_id, updater_fun)} + end + + @impl true + def handle_call({:entries, date}, _, state) do + {:reply, TodoList.entries(state, date), state} + end +end + +defmodule TodoList do + defstruct auto_id: 1, entries: %{} + + def new(), do: %TodoList{} + + def add_entry(todo_list, entry) do + entry = Map.put(entry, :id, todo_list.auto_id) + + new_entries = + Map.put( + todo_list.entries, + todo_list.auto_id, + entry + ) + + %TodoList{todo_list | entries: new_entries, auto_id: todo_list.auto_id + 1} + end + + def entries(todo_list, date) do + todo_list.entries + |> Stream.filter(fn {_, entry} -> entry.date == date end) + |> Enum.map(fn {_, entry} -> entry end) + end + + def update_entry(todo_list, %{} = new_entry) do + update_entry(todo_list, new_entry.id, fn _ -> new_entry end) + end + + def update_entry(todo_list, entry_id, updater_fun) do + case Map.fetch(todo_list.entries, entry_id) do + :error -> + todo_list + + {:ok, old_entry} -> + old_entry_id = old_entry.id + # Make sure that the result of the updater is a map and the + # id remains unchanged. + new_entry = %{id: ^old_entry_id} = updater_fun.(old_entry) + new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) + %TodoList{todo_list | entries: new_entries} + end + end + + def delete_entry(todo_list, entry_id) do + %TodoList{todo_list | entries: Map.delete(todo_list.entries, entry_id)} + end +end