Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Chapter 5

Chapters/Chapter_5.livemd

Chapter 5

Section

# run_query =
#   fn query_def ->
#     Process.sleep(2000)
#     "#{query_def} result"
#   end

# # Enum.map(
# #   1..5,
# #   fn index ->
# #     query_def = "query #{index}"
# #     run_query.(query_def)
# #   end
# # )

# # spawn(fn ->
# #   query_result = run_query.("query 1")
# #   IO.puts(query_result)
# # end)

# async_query =
#   fn query_def ->
#     spawn(fn ->
#       query_result = run_query.(query_def)
#       IO.puts(query_result)
#     end)
#   end

# Enum.each(1..5, &async_query.("query #{&1}"))
# send(self(), "a message")

# receive do
#   message -> IO.inspect(message)
# end

# receive do
#   message -> IO.inspect(message)
# after
#   5000 -> IO.puts("message not received")
# end
# Now lets try to send a bunch of messages and send them to a unique proces
# to collect.

# async_query =
#   fn query_def ->
#     # Stores the Pid in the main calling process
#     caller = self()

#     spawn(fn ->
#       query_result = run_query.(query_def)
#       # Sends a response
#       send(caller, {:query_result, query_result})
#     end)
#   end

# Enum.each(1..5, &async_query.("query #{&1}"))

# get_result =
#   fn ->
#     receive do
#       {:query_result, result} -> result
#     end
#   end

# # results = Enum.map(1..5, fn _ -> get_result.() end)

# 1..5 # Same but with a pipeline
# |> Enum.map(&async_query.("query #{&1}"))
# |> Enum.map(fn _ -> get_result.() end)
defmodule DatabaseServer do
  def start do
    spawn(fn ->
      connection = :rand.uniform(1000)
      loop(connection)
    end)
  end

  def run_async(server_pid, query_def) do
    send(server_pid, {:run_query, self(), query_def})
  end

  def get_result do
    receive do
      {:query_result, result} -> result
    after
      5000 -> {:error, :timeout}
    end
  end

  defp loop(connection) do
    receive do
      {:run_query, caller, query_def} ->
        query_result = run_query(connection, query_def)
        send(caller, {:query_result, query_result})
    end

    loop(connection)
  end

  defp run_query(connection, query_def) do
    Process.sleep(2000)
    "Connection #{connection}: #{query_def} result"
  end
end
# server_pid = DatabaseServer.start()

# DatabaseServer.run_async(server_pid, "query 1")
# DatabaseServer.get_result()
# pool = Enum.map(1..100, fn _ -> DatabaseServer.start() end)

# Enum.each(
#   1..5,
#   fn query_def ->
#     server_pid = Enum.at(pool, :rand.uniform(100) - 1)
#     DatabaseServer.run_async(server_pid, query_def)
#   end
# )

# Enum.map(1..5, fn _ -> DatabaseServer.get_result() end)

server_pid = DatabaseServer.start()
DatabaseServer.run_async(server_pid, "query 1")
DatabaseServer.get_result()

DatabaseServer.run_async(server_pid, "query 2")
DatabaseServer.get_result()
# Passing a state with the loop 
# def start do
#   spawn(fn ->
#     initial_state = ...
#     loop(initial_state)
#   end)
# end

# defp loop(state) do
#   ...
#   loop(state)
# end
# Mutable state
# defp loop(state) do
#   new_state = # Sets the new state based off the message
#     receive do
#       msg1 ->
#         ...

#       msg2 ->
#         ...
#     end

#   loop(new_state) # makes sure to pass the new state to the loop
# end
defmodule Calculator do
  @moduledoc """
  This module will start a new calculator that will hold the current value
  for any arithmetic expression and value you send to it.

  iex(1)> calculator_pid = Calculator.start()
  iex(2)> Calculator.value(calculator_pid)
  0
  iex(3)> Calculator.add(calculator_pid, 10)
  iex(4)> Calculator.sub(calculator_pid, 5)
  iex(5)> Calculator.mul(calculator_pid, 3)
  iex(6)> Calculator.div(calculator_pid, 5)
  iex(7)> Calculator.value(calculator_pid)
  3.0
  iex(8)> Calculator.clear(calculator_pid)
  iex(9)> Calculator.value(calculator_pid)
  0
  """

  def start do
    spawn(fn ->
      loop(0)
    end)
  end

  def value(server_pid) do
    send(server_pid, {:value, self()})

    receive do
      {:response, value} ->
        value
    end
  end

  def add(server_pid, value), do: send(server_pid, {:add, value})
  def sub(server_pid, value), do: send(server_pid, {:sub, value})
  def mul(server_pid, value), do: send(server_pid, {:mul, value})
  def div(server_pid, value), do: send(server_pid, {:div, value})
  def clear(server_pid), do: send(server_pid, {:clear})

  defp loop(current_value) do
    new_value =
      receive do
        message -> process_message(current_value, message)
      end

    loop(new_value)
  end

  defp process_message(current_value, {:value, caller}) do
    send(caller, {:response, current_value})
    current_value
  end

  defp process_message(current_value, {:add, value}), do: current_value + value
  defp process_message(current_value, {:sub, value}), do: current_value - value
  defp process_message(current_value, {:mul, value}), do: current_value * value
  defp process_message(current_value, {:div, value}), do: current_value / value
  defp process_message(_current_value, {:clear}), do: 0

  # defp loop(current_value) do
  #   new_value =
  #     receive do
  #       {:value, caller} ->
  #         send(caller, {:responce, current_value})

  #       {:add, value} ->
  #         current_value + value

  #       {:sub, value} ->
  #         current_value - value

  #       {:mul, value} ->
  #         current_value * value

  #       {:div, value} ->
  #         current_value / value

  #       {:clear, _} ->
  #         current_value = 0

  #       invalid_request ->
  #         IO.puts("invalid request: #{inspect(invalid_request)}")
  #         current_value
  #     end

  #   loop(new_value)
  # end
end
# Let's talk about a more complex state. Let's keep track of a TodoList.
defmodule TodoList do
  defstruct next_id: 1, entries: %{}

  # def new(), do: %TodoList{}

  def new(entries \\ []) do
    Enum.reduce(entries, %TodoList{}, fn entry, todo_list ->
      add_entry(todo_list, entry)
    end)

    # entires, %TodoList{}, &add_entry(&2, &1)
    # Needs to be in this order as the lambda will pass the entry then the acc.
  end

  def add_entry(todo_list, entry) do
    # adds the id to the entry
    entry = Map.put(entry, :id, todo_list.next_id)

    new_entries =
      Map.put(
        todo_list.entries,
        todo_list.next_id,
        entry
      )

    # adds the new entry to the todo_list

    # updates the next id
    %TodoList{todo_list | entries: new_entries, next_id: todo_list.next_id + 1}
  end

  def entries(todo_list, date) do
    todo_list.entries
    |> Map.values()
    |> Enum.filter(fn entry -> entry.date == date end)
  end

  def entries(todo_list) do
    todo_list.entries
    |> Map.values()
  end

  def update_entries(todo_list, entry_id, updater_fun) do
    case Map.fetch(todo_list.entries, entry_id) do
      :error ->
        todo_list

      {:ok, old_entry} ->
        new_entry = 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
    case Map.fetch(todo_list.entries, entry_id) do
      :error ->
        IO.inspect("cant find")
        todo_list

      {:ok, _entry} ->
        updated_entries = Map.delete(todo_list.entries, entry_id)
        %TodoList{todo_list | entries: updated_entries}
    end
  end
end
defmodule TodoServer do
  @moduledoc """
  This will be used to have a server that will take and process TodoList items. 
  """
  def start do
    spawn(fn -> loop(TodoList.new()) end)
  end

  def loop(todo_list) do
    new_todo_list =
      receive do
        message -> process_message(todo_list, message)
      end

    loop(new_todo_list)
  end

  def entries(todo_server) do
    send(todo_server, {:entries, self()})

    receive do
      {:todo_entires, entries} -> entries
    after
      5000 -> {:error, :timeout}
    end
  end

  def entries(todo_server, date) do
    send(todo_server, {:entries, self(), date})

    receive do
      {:todo_entires, entries} -> entries
    after
      5000 -> {:error, :timeout}
    end
  end

  def add_entry(todo_server, new_entry) do
    send(todo_server, {:add_entry, new_entry})
  end

  defp process_message(todo_list, {:entries, caller, date}) do
    send(caller, {:todo_entires, TodoList.entries(todo_list, date)})
    todo_list
  end

  defp process_message(todo_list, {:entries, caller}) do
    send(caller, {:todo_entires, TodoList.entries(todo_list)})
    todo_list
  end

  defp process_message(todo_list, {:add_entry, new_entry}) do
    TodoList.add_entry(todo_list, new_entry)
  end
end
todo_server = TodoServer.start()

TodoServer.add_entry(
  todo_server,
  %{date: ~D[2023-12-19], title: "Dentist"}
)

TodoServer.add_entry(
  todo_server,
  %{date: ~D[2023-12-20], title: "Shopping"}
)

TodoServer.add_entry(
  todo_server,
  %{date: ~D[2023-12-19], title: "Movies"}
)

TodoServer.entries(todo_server, ~D[2023-12-19])

[
  %{date: ~D[2023-12-19], id: 3, title: "Movies"},
  %{date: ~D[2023-12-19], id: 1, title: "Dentist"}
]

TodoServer.entries(todo_server)
# What happens when you are not using a single processe and you need 
# to talk to others. Ypu can use...
# Process.register(self(), :some_name)
# send(:some_name, :msg)

# receive do
#   msg -> IO.puts("received #{msg}")
# end

# received msg
defmodule TodoServer.PIDLess do
  @moduledoc """
  This will be used to have a server that will take and process TodoList items. 
  """
  def start do
    spawn(fn ->
      # Only register if not already taken
      unless Process.whereis(:todo_server) do
        Process.register(self(), :todo_server)
      end

      loop(TodoList.new())
    end)
  end

  def loop(todo_list) do
    new_todo_list =
      receive do
        message -> process_message(todo_list, message)
      end

    loop(new_todo_list)
  end

  def clear do
    send(:todo_server, {:clear})
  end

  def entries do
    send(:todo_server, {:entries, self()})

    receive do
      {:todo_entires, entries} -> entries
    after
      5000 -> {:error, :timeout}
    end
  end

  def entries(date) do
    send(:todo_server, {:entries, self(), date})

    receive do
      {:todo_entires, entries} -> entries
    after
      5000 -> {:error, :timeout}
    end
  end

  def add_entry(new_entry) do
    send(:todo_server, {:add_entry, new_entry})
  end

  defp process_message(_todo_list, {:clear}) do
    TodoList.new()
  end

  defp process_message(todo_list, {:entries, caller, date}) do
    send(caller, {:todo_entires, TodoList.entries(todo_list, date)})
    todo_list
  end

  defp process_message(todo_list, {:entries, caller}) do
    send(caller, {:todo_entires, TodoList.entries(todo_list)})
    todo_list
  end

  defp process_message(todo_list, {:add_entry, new_entry}) do
    TodoList.add_entry(todo_list, new_entry)
  end
end
# here is the use of the TodoServer.PIDLess
TodoServer.PIDLess.start()
TodoServer.PIDLess.add_entry(%{date: ~D[2023-12-19], title: "Dentist"})
TodoServer.PIDLess.add_entry(%{date: ~D[2023-12-20], title: "Shopping"})
TodoServer.PIDLess.add_entry(%{date: ~D[2023-12-19], title: "Movies"})
TodoServer.PIDLess.add_entry(%{date: ~D[2023-12-19], title: "Dentist"})
TodoServer.PIDLess.add_entry(%{date: ~D[2023-12-20], title: "Shopping"})
TodoServer.PIDLess.add_entry(%{date: ~D[2023-12-19], title: "Movies"})
TodoServer.PIDLess.add_entry(%{date: ~D[2023-12-19], title: "Dentist"})
TodoServer.PIDLess.add_entry(%{date: ~D[2023-12-20], title: "Shopping"})
TodoServer.PIDLess.add_entry(%{date: ~D[2023-12-19], title: "Movies"})
TodoServer.PIDLess.entries()
|> IO.inspect(label: "Full list")

TodoServer.PIDLess.clear()
TodoServer.PIDLess.entries()
defmodule Server do
  @moduledoc """
  This is a very slow response server look at the response times in the cell below
  """
  def start do
    spawn(fn -> loop() end)
  end

  def send_msg(server, message) do
    send(server, {self(), message})

    receive do
      {:response, response} -> response
    end
  end

  defp loop do
    receive do
      {caller, msg} ->
        Process.sleep(1000)
        send(caller, {:response, msg})
    end

    loop()
  end
end
server = Server.start()

Enum.each(
  1..5,
  fn i ->
    spawn(fn ->
      IO.puts("Sending msg ##{i}")
      response = Server.send_msg(server, i)
      IO.puts("Response: #{response}")
    end)
  end
)