$30 off During Our Annual Pro Sale. View Details »

How to Level Up in Elixir

jeg2
June 23, 2021

How to Level Up in Elixir

The July 2021 talk at Denver Erlang and Elixir by James Edward Gray II.

jeg2

June 23, 2021
Tweet

More Decks by jeg2

Other Decks in Programming

Transcript

  1. How to Level Up in Elixir
    James Edward Gray II — 6/5/2021
    Designing a Chat Application

    View Slide

  2. James Edward Gray II
    • A longtime Rubyist


    • Creator of the Ruby Rogues
    podcast


    • An Elixir community regular for
    years now


    • I wrote a book about Elixir
    @JEG2

    View Slide

  3. I Work With Brett
    Who is on vacation…


    while I'm running


    his group?!

    View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. How would


    you do it?
    The Evils of


    Thought Leadership

    View Slide

  9. The Communication Layer

    View Slide

  10. Socket Programming
    • A program has one "socket" connection with each remote system it is
    communicating with


    • Sockets are two-way, supporting both the sending and receiving of data


    • Data can arrive on any socket at anytime

    View Slide

  11. Multitasking

    View Slide

  12. Starting Processes
    Conway's Game of Life


    with one process


    per cell
    All socket handling


    in one GenServer

    View Slide

  13. Starting Processes
    Conway's Game of Life


    with one process


    per cell
    All socket handling


    in one GenServer

    View Slide

  14. Supervisor
    Supervisor
    ConnectionManager

    View Slide

  15. Supervisor
    Supervisor
    ConnectionManager
    Listener
    Connection
    Connection

    View Slide

  16. defmodule ChatApp.Application do
    @moduledoc false
    use Application
    @impl true
    def start(_type, _args) do
    children = [
    {
    DynamicSupervisor
    ,

    strategy: :one_for_one, name: ChatApp.ConnectionSupervisor
    }
    ,

    {ChatApp.ConnectionManager, ui: ChatApp.GUI}
    ,

    ChatApp.GUI
    ]
    opts = [strategy: :one_for_one, name: ChatApp.Supervisor]
    Supervisor.start_link(children, opts)
    end
    end

    View Slide

  17. defmodule ChatApp.Application do
    @moduledoc false
    use Application
    @impl true
    def start(_type, _args) do
    children = [
    {
    DynamicSupervisor
    ,

    strategy: :one_for_one, name: ChatApp.ConnectionSupervisor
    }
    ,

    {ChatApp.ConnectionManager, ui: ChatApp.GUI}
    ,

    ChatApp.GUI
    ]
    opts = [strategy: :one_for_one, name: ChatApp.Supervisor]
    Supervisor.start_link(children, opts)
    end
    end

    View Slide

  18. defmodule ChatApp.ConnectionManager do
    use GenServer
    alias ChatApp.{Connection, ConnectionSupervisor, Listener}
    # ..
    .

    defstruct mode: :ready, name: nil, ui: nil, me: nil
    def start_link(options) do
    GenServer.start_link(
    __MODULE__
    ,

    options
    ,

    name: Keyword.get(options, :name, __MODULE__)
    )
    end
    def init(options) do
    case Keyword.get(options, :ui) do
    ui when is_atom(ui) -
    >

    me = Keyword.get(options, :name, __MODULE__) || self()
    {:ok, %__MODULE__{ui: ui, me: me}}
    _no_ui -
    >

    {:stop, "ConnectionManager must be started with a UI module"}
    end
    end
    end

    View Slide

  19. defmodule ChatApp.ConnectionManager do
    use GenServer
    alias ChatApp.{Connection, ConnectionSupervisor, Listener}
    # ..
    .

    defstruct mode: :ready, name: nil, ui: nil, me: nil
    def start_link(options) do
    GenServer.start_link(
    __MODULE__
    ,

    options
    ,

    name: Keyword.get(options, :name, __MODULE__)
    )
    end
    def init(options) do
    case Keyword.get(options, :ui) do
    ui when is_atom(ui) -
    >

    me = Keyword.get(options, :name, __MODULE__) || self()
    {:ok, %__MODULE__{ui: ui, me: me}}
    _no_ui -
    >

    {:stop, "ConnectionManager must be started with a UI module"}
    end
    end
    end

    View Slide

  20. defmodule ChatApp.ConnectionManager do
    use GenServer
    alias ChatApp.{Connection, ConnectionSupervisor, Listener}
    # ..
    .

    defstruct mode: :ready, name: nil, ui: nil, me: nil
    def start_link(options) do
    GenServer.start_link(
    __MODULE__
    ,

    options
    ,

    name: Keyword.get(options, :name, __MODULE__)
    )
    end
    def init(options) do
    case Keyword.get(options, :ui) do
    ui when is_atom(ui) -
    >

    me = Keyword.get(options, :name, __MODULE__) || self()
    {:ok, %__MODULE__{ui: ui, me: me}}
    _no_ui -
    >

    {:stop, "ConnectionManager must be started with a UI module"}
    end
    end
    end

    View Slide

  21. defmodule ChatApp.ConnectionManager do
    use GenServer
    alias ChatApp.{Connection, ConnectionSupervisor, Listener}
    # ..
    .

    defstruct mode: :ready, name: nil, ui: nil, me: nil
    def start_link(options) do
    GenServer.start_link(
    __MODULE__
    ,

    options
    ,

    name: Keyword.get(options, :name, __MODULE__)
    )
    end
    def init(options) do
    case Keyword.get(options, :ui) do
    ui when is_atom(ui) -
    >

    me = Keyword.get(options, :name, __MODULE__) || self()
    {:ok, %__MODULE__{ui: ui, me: me}}
    _no_ui -
    >

    {:stop, "ConnectionManager must be started with a UI module"}
    end
    end
    end

    View Slide

  22. defmodule ChatApp.ConnectionManager do
    use GenServer
    alias ChatApp.{Connection, ConnectionSupervisor, Listener}
    # ..
    .

    defstruct mode: :ready, name: nil, ui: nil, me: nil
    def start_link(options) do
    GenServer.start_link(
    __MODULE__
    ,

    options
    ,

    name: Keyword.get(options, :name, __MODULE__)
    )
    end
    def init(options) do
    case Keyword.get(options, :ui) do
    ui when is_atom(ui) -
    >

    me = Keyword.get(options, :name, __MODULE__) || self()
    {:ok, %__MODULE__{ui: ui, me: me}}
    _no_ui -
    >

    {:stop, "ConnectionManager must be started with a UI module"}
    end
    end
    end

    View Slide

  23. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def listen(manager \\ __MODULE__, port, name) do
    GenServer.call(manager, {:listen, port, name})
    end
    def handle_call({:listen, port, name}, _from, state) do
    case start_listening(port, state.me) do
    :ok -
    >

    {:reply, :ok, %__MODULE__{state | mode: :host, name: name}}
    error -
    >

    {:reply, error, state}
    end
    end
    end

    View Slide

  24. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def listen(manager \\ __MODULE__, port, name) do
    GenServer.call(manager, {:listen, port, name})
    end
    def handle_call({:listen, port, name}, _from, state) do
    case start_listening(port, state.me) do
    :ok -
    >

    {:reply, :ok, %__MODULE__{state | mode: :host, name: name}}
    error -
    >

    {:reply, error, state}
    end
    end
    end

    View Slide

  25. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def listen(manager \\ __MODULE__, port, name) do
    GenServer.call(manager, {:listen, port, name})
    end
    def handle_call({:listen, port, name}, _from, state) do
    case start_listening(port, state.me) do
    :ok -
    >

    {:reply, :ok, %__MODULE__{state | mode: :host, name: name}}
    error -
    >

    {:reply, error, state}
    end
    end
    end

    View Slide

  26. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def listen(manager \\ __MODULE__, port, name) do
    GenServer.call(manager, {:listen, port, name})
    end
    def handle_call({:listen, port, name}, _from, state) do
    case start_listening(port, state.me) do
    :ok -
    >

    {:reply, :ok, %__MODULE__{state | mode: :host, name: name}}
    error -
    >

    {:reply, error, state}
    end
    end
    end

    View Slide

  27. Listening for Connections
    • A listening socket queues connections as they come in


    • A program can "accept" them to complete the connection


    • The result of accepting a connection is a read/write socket

    used to communicate with the remote program


    • Erlang (now) provides a low-level `socket`,

    but the higher-level `gen_tcp` adds several niceties

    View Slide

  28. defmodule ChatApp.ConnectionManager do
    # ..
    .

    @packet_size 2
    defp start_listening(port, me) do
    case :gen_tcp.listen(
    port
    ,

    [:binary, packet: @packet_size, active: false, reuseaddr: true]
    ) do
    {:ok, listening_socket} -
    >

    Listener.listen(listening_socket, me)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  29. defmodule ChatApp.ConnectionManager do
    # ..
    .

    @packet_size 2
    defp start_listening(port, me) do
    case :gen_tcp.listen(
    port
    ,

    [:binary, packet: @packet_size, active: false, reuseaddr: true]
    ) do
    {:ok, listening_socket} -
    >

    Listener.listen(listening_socket, me)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  30. defmodule ChatApp.ConnectionManager do
    # ..
    .

    @packet_size 2
    defp start_listening(port, me) do
    case :gen_tcp.listen(
    port
    ,

    [:binary, packet: @packet_size, active: false, reuseaddr: true]
    ) do
    {:ok, listening_socket} -
    >

    Listener.listen(listening_socket, me)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  31. defmodule ChatApp.ConnectionManager do
    # ..
    .

    @packet_size 2
    defp start_listening(port, me) do
    case :gen_tcp.listen(
    port
    ,

    [:binary, packet: @packet_size, active: false, reuseaddr: true]
    ) do
    {:ok, listening_socket} -
    >

    Listener.listen(listening_socket, me)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  32. defmodule ChatApp.Listener do
    use GenServer, restart: :transient
    alias ChatApp.{Connection, ConnectionSupervisor}
    # ..
    .

    def listen(listening_socket, manager) do
    case DynamicSupervisor.start_child(
    ConnectionSupervisor
    ,

    {__MODULE__, [listening_socket, manager]}
    ) do
    {:ok, listener} -
    >

    transfer_control(listening_socket, listener)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  33. defmodule ChatApp.Listener do
    use GenServer, restart: :transient
    alias ChatApp.{Connection, ConnectionSupervisor}
    # ..
    .

    def listen(listening_socket, manager) do
    case DynamicSupervisor.start_child(
    ConnectionSupervisor
    ,

    {__MODULE__, [listening_socket, manager]}
    ) do
    {:ok, listener} -
    >

    transfer_control(listening_socket, listener)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  34. defmodule ChatApp.Listener do
    use GenServer, restart: :transient
    alias ChatApp.{Connection, ConnectionSupervisor}
    # ..
    .

    def listen(listening_socket, manager) do
    case DynamicSupervisor.start_child(
    ConnectionSupervisor
    ,

    {__MODULE__, [listening_socket, manager]}
    ) do
    {:ok, listener} -
    >

    transfer_control(listening_socket, listener)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  35. defmodule ChatApp.Listener do
    # ..
    .

    defp transfer_control(listening_socket, listener) do
    case :gen_tcp.controlling_process(listening_socket, listener) do
    :ok -
    >

    accept(listener)
    :ok
    error -
    >

    close(listener)
    erro
    r

    end
    end
    defp accept(listener), do: GenServer.cast(listener, :accept)
    end

    View Slide

  36. defmodule ChatApp.Listener do
    # ..
    .

    defp transfer_control(listening_socket, listener) do
    case :gen_tcp.controlling_process(listening_socket, listener) do
    :ok -
    >

    accept(listener)
    :ok
    error -
    >

    close(listener)
    erro
    r

    end
    end
    defp accept(listener), do: GenServer.cast(listener, :accept)
    end

    View Slide

  37. defmodule ChatApp.Listener do
    # ..
    .

    defp transfer_control(listening_socket, listener) do
    case :gen_tcp.controlling_process(listening_socket, listener) do
    :ok -
    >

    accept(listener)
    :ok
    error -
    >

    close(listener)
    erro
    r

    end
    end
    defp accept(listener), do: GenServer.cast(listener, :accept)
    end

    View Slide

  38. defmodule ChatApp.Listener do
    # ..
    .

    defp transfer_control(listening_socket, listener) do
    case :gen_tcp.controlling_process(listening_socket, listener) do
    :ok -
    >

    accept(listener)
    :ok
    error -
    >

    close(listener)
    erro
    r

    end
    end
    defp accept(listener), do: GenServer.cast(listener, :accept)
    end

    View Slide

  39. A `GenServer` Loop
    • Keep triggering `handle_cast/
    2` or `handle_info/2`


    • Timeout long running code or
    execute it in a linked `Task`


    • `Process.send_after/2` and
    `:timer.send_interval/2` can
    repeat message sends


    • Process other messages
    between calls
    Danger Zone!

    View Slide

  40. defmodule ChatApp.Listener do
    # ..
    .

    def handle_cast(:accept, state) do
    case :gen_tcp.accept(state.listening_socket, 1_000) do
    {:ok, socket} -
    >

    Connection.listen(socket, state.manager)
    accept(self())
    {:noreply, state}
    {:error, :closed} -
    >

    {:stop, :normal, %__MODULE__{state | listening_socket: nil}}
    _error -
    >

    accept(self())
    {:noreply, state}
    end
    end
    end

    View Slide

  41. defmodule ChatApp.Listener do
    # ..
    .

    def handle_cast(:accept, state) do
    case :gen_tcp.accept(state.listening_socket, 1_000) do
    {:ok, socket} -
    >

    Connection.listen(socket, state.manager)
    accept(self())
    {:noreply, state}
    {:error, :closed} -
    >

    {:stop, :normal, %__MODULE__{state | listening_socket: nil}}
    _error -
    >

    accept(self())
    {:noreply, state}
    end
    end
    end

    View Slide

  42. defmodule ChatApp.Listener do
    # ..
    .

    def handle_cast(:accept, state) do
    case :gen_tcp.accept(state.listening_socket, 1_000) do
    {:ok, socket} -
    >

    Connection.listen(socket, state.manager)
    accept(self())
    {:noreply, state}
    {:error, :closed} -
    >

    {:stop, :normal, %__MODULE__{state | listening_socket: nil}}
    _error -
    >

    accept(self())
    {:noreply, state}
    end
    end
    end

    View Slide

  43. defmodule ChatApp.Listener do
    # ..
    .

    def handle_cast(:accept, state) do
    case :gen_tcp.accept(state.listening_socket, 1_000) do
    {:ok, socket} -
    >

    Connection.listen(socket, state.manager)
    accept(self())
    {:noreply, state}
    {:error, :closed} -
    >

    {:stop, :normal, %__MODULE__{state | listening_socket: nil}}
    _error -
    >

    accept(self())
    {:noreply, state}
    end
    end
    end

    View Slide

  44. defmodule ChatApp.Listener do
    # ..
    .

    def handle_cast(:accept, state) do
    case :gen_tcp.accept(state.listening_socket, 1_000) do
    {:ok, socket} -
    >

    Connection.listen(socket, state.manager)
    accept(self())
    {:noreply, state}
    {:error, :closed} -
    >

    {:stop, :normal, %__MODULE__{state | listening_socket: nil}}
    _error -
    >

    accept(self())
    {:noreply, state}
    end
    end
    end

    View Slide

  45. defmodule ChatApp.Listener do
    # ..
    .

    defstruct ~w[listening_socket manager]
    a

    def start_link([listening_socket, manager]) do
    GenServer.start_link(__MODULE__, [listening_socket, manager])
    end
    def close(listener), do: GenServer.cast(listener, :close)
    def init([listening_socket, manager]) do
    {:ok, %__MODULE__{listening_socket: listening_socket, manager: manager}}
    end
    def handle_cast(:close, state) do
    :gen_tcp.close(state.listening_socket)
    {:stop, :normal, %__MODULE__{state | listening_socket: nil}}
    end
    end

    View Slide

  46. defmodule ChatApp.Listener do
    # ..
    .

    defstruct ~w[listening_socket manager]
    a

    def start_link([listening_socket, manager]) do
    GenServer.start_link(__MODULE__, [listening_socket, manager])
    end
    def close(listener), do: GenServer.cast(listener, :close)
    def init([listening_socket, manager]) do
    {:ok, %__MODULE__{listening_socket: listening_socket, manager: manager}}
    end
    def handle_cast(:close, state) do
    :gen_tcp.close(state.listening_socket)
    {:stop, :normal, %__MODULE__{state | listening_socket: nil}}
    end
    end

    View Slide

  47. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def connect(manager \\ __MODULE__, host, port, name) do
    GenServer.call(manager, {:connect, host, port, name})
    end
    def handle_call({:connect, host, port, name}, _from, state) do
    case listen_to_connection(host, port, state.me) do
    :ok -
    >

    new_state = %__MODULE__{state | mode: :client, name: name}
    queue_all_sends(:connected, new_state)
    {:reply, :ok, new_state}
    error -
    >

    {:reply, error, state}
    end
    end
    end

    View Slide

  48. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def connect(manager \\ __MODULE__, host, port, name) do
    GenServer.call(manager, {:connect, host, port, name})
    end
    def handle_call({:connect, host, port, name}, _from, state) do
    case listen_to_connection(host, port, state.me) do
    :ok -
    >

    new_state = %__MODULE__{state | mode: :client, name: name}
    queue_all_sends(:connected, new_state)
    {:reply, :ok, new_state}
    error -
    >

    {:reply, error, state}
    end
    end
    end

    View Slide

  49. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def connect(manager \\ __MODULE__, host, port, name) do
    GenServer.call(manager, {:connect, host, port, name})
    end
    def handle_call({:connect, host, port, name}, _from, state) do
    case listen_to_connection(host, port, state.me) do
    :ok -
    >

    new_state = %__MODULE__{state | mode: :client, name: name}
    queue_all_sends(:connected, new_state)
    {:reply, :ok, new_state}
    error -
    >

    {:reply, error, state}
    end
    end
    end

    View Slide

  50. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def connect(manager \\ __MODULE__, host, port, name) do
    GenServer.call(manager, {:connect, host, port, name})
    end
    def handle_call({:connect, host, port, name}, _from, state) do
    case listen_to_connection(host, port, state.me) do
    :ok -
    >

    new_state = %__MODULE__{state | mode: :client, name: name}
    queue_all_sends(:connected, new_state)
    {:reply, :ok, new_state}
    error -
    >

    {:reply, error, state}
    end
    end
    end

    View Slide

  51. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp listen_to_connection(host, port, me) do
    case :gen_tcp.connect(
    String.to_charlist(host)
    ,

    port
    ,

    [:binary, packet: @packet_size, active: false]
    ) do
    {:ok, socket} -
    >

    Connection.listen(socket, me)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  52. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp listen_to_connection(host, port, me) do
    case :gen_tcp.connect(
    String.to_charlist(host)
    ,

    port
    ,

    [:binary, packet: @packet_size, active: false]
    ) do
    {:ok, socket} -
    >

    Connection.listen(socket, me)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  53. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp listen_to_connection(host, port, me) do
    case :gen_tcp.connect(
    String.to_charlist(host)
    ,

    port
    ,

    [:binary, packet: @packet_size, active: false]
    ) do
    {:ok, socket} -
    >

    Connection.listen(socket, me)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  54. Protocols in Protocols
    • TCP/IP is a protocol for reliably delivering messages over an unreliable
    network


    • How do we know what a full message is though?


    • Delimit messages with something like newlines


    • Send the length of the message, then the message

    (`:gen_tcp` does this!)


    • How do we know what's in a message?


    • Erlang's `term_to_binary/1`

    View Slide

  55. View Slide

  56. View Slide

  57. View Slide

  58. View Slide

  59. View Slide

  60. View Slide

  61. View Slide

  62. defmodule ChatApp.Connection do
    use GenServer, restart: :transient
    alias ChatApp.{ConnectionManager, ConnectionSupervisor}
    # ..
    .

    defstruct ~w[socket manager]
    a

    def start_link([socket, manager]) do
    GenServer.start_link(__MODULE__, [socket, manager])
    end
    def close(connection), do: GenServer.cast(connection, :close)
    def init([socket, manager]) do
    {:ok, %__MODULE__{socket: socket, manager: manager}}
    end
    def handle_cast(:close, state) do
    :gen_tcp.close(state.socket)
    {:stop, :normal, %__MODULE__{state | socket: nil}}
    end
    end

    View Slide

  63. defmodule ChatApp.Connection do
    use GenServer, restart: :transient
    alias ChatApp.{ConnectionManager, ConnectionSupervisor}
    # ..
    .

    defstruct ~w[socket manager]
    a

    def start_link([socket, manager]) do
    GenServer.start_link(__MODULE__, [socket, manager])
    end
    def close(connection), do: GenServer.cast(connection, :close)
    def init([socket, manager]) do
    {:ok, %__MODULE__{socket: socket, manager: manager}}
    end
    def handle_cast(:close, state) do
    :gen_tcp.close(state.socket)
    {:stop, :normal, %__MODULE__{state | socket: nil}}
    end
    end

    View Slide

  64. defmodule ChatApp.Connection do
    # ..
    .

    def listen(socket, manager) do
    case DynamicSupervisor.start_child(
    ConnectionSupervisor
    ,

    {__MODULE__, [socket, manager]}
    ) do
    {:ok, connection} -
    >

    transfer_control(socket, connection)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  65. defmodule ChatApp.Connection do
    # ..
    .

    def listen(socket, manager) do
    case DynamicSupervisor.start_child(
    ConnectionSupervisor
    ,

    {__MODULE__, [socket, manager]}
    ) do
    {:ok, connection} -
    >

    transfer_control(socket, connection)
    error -
    >

    erro
    r

    end
    end
    end

    View Slide

  66. defmodule ChatApp.Connection do
    # ..
    .

    def handle_cast(:activate, state) do
    :inet.setopts(state.socket, active: :once)
    {:noreply, state}
    end
    defp transfer_control(socket, connection) do
    case :gen_tcp.controlling_process(socket, connection) do
    :ok -
    >

    activate(connection)
    :ok
    error -
    >

    close(connection)
    erro
    r

    end
    end
    defp activate(connection), do: GenServer.cast(connection, :activate)
    end

    View Slide

  67. defmodule ChatApp.Connection do
    # ..
    .

    def handle_cast(:activate, state) do
    :inet.setopts(state.socket, active: :once)
    {:noreply, state}
    end
    defp transfer_control(socket, connection) do
    case :gen_tcp.controlling_process(socket, connection) do
    :ok -
    >

    activate(connection)
    :ok
    error -
    >

    close(connection)
    erro
    r

    end
    end
    defp activate(connection), do: GenServer.cast(connection, :activate)
    end

    View Slide

  68. defmodule ChatApp.Connection do
    # ..
    .

    def handle_cast(:activate, state) do
    :inet.setopts(state.socket, active: :once)
    {:noreply, state}
    end
    defp transfer_control(socket, connection) do
    case :gen_tcp.controlling_process(socket, connection) do
    :ok -
    >

    activate(connection)
    :ok
    error -
    >

    close(connection)
    erro
    r

    end
    end
    defp activate(connection), do: GenServer.cast(connection, :activate)
    end

    View Slide

  69. TCP Event Messages
    • `gen_tcp` can deliver events—data arriving, a socket closing,

    and the like—as messages to a process


    • Of course, a process could drown in incoming messages


    • `active: :once` controls the
    f
    low

    View Slide

  70. defmodule ChatApp.Connection do
    # ..
    .

    def handle_info({:tcp_closed, _socket}, state) do
    {:stop, :normal, %__MODULE__{state | socket: nil}}
    end
    def handle_info({:tcp, _socket, message}, state) do
    ConnectionManager.receive_message(state.manager, message, self())
    activate(self())
    {:noreply, state}
    end
    def handle_info(_unexpected_message, state), do: {:noreply, state}
    end

    View Slide

  71. defmodule ChatApp.Connection do
    # ..
    .

    def handle_info({:tcp_closed, _socket}, state) do
    {:stop, :normal, %__MODULE__{state | socket: nil}}
    end
    def handle_info({:tcp, _socket, message}, state) do
    ConnectionManager.receive_message(state.manager, message, self())
    activate(self())
    {:noreply, state}
    end
    def handle_info(_unexpected_message, state), do: {:noreply, state}
    end

    View Slide

  72. defmodule ChatApp.Connection do
    # ..
    .

    def handle_info({:tcp_closed, _socket}, state) do
    {:stop, :normal, %__MODULE__{state | socket: nil}}
    end
    def handle_info({:tcp, _socket, message}, state) do
    ConnectionManager.receive_message(state.manager, message, self())
    activate(self())
    {:noreply, state}
    end
    def handle_info(_unexpected_message, state), do: {:noreply, state}
    end

    View Slide

  73. defmodule ChatApp.Connection do
    # ..
    .

    def handle_info({:tcp_closed, _socket}, state) do
    {:stop, :normal, %__MODULE__{state | socket: nil}}
    end
    def handle_info({:tcp, _socket, message}, state) do
    ConnectionManager.receive_message(state.manager, message, self())
    activate(self())
    {:noreply, state}
    end
    def handle_info(_unexpected_message, state), do: {:noreply, state}
    end

    View Slide

  74. defmodule ChatApp.Connection do
    # ..
    .

    def handle_info({:tcp_closed, _socket}, state) do
    {:stop, :normal, %__MODULE__{state | socket: nil}}
    end
    def handle_info({:tcp, _socket, message}, state) do
    ConnectionManager.receive_message(state.manager, message, self())
    activate(self())
    {:noreply, state}
    end
    def handle_info(_unexpected_message, state), do: {:noreply, state}
    end

    View Slide

  75. defmodule ChatApp.Connection do
    # ..
    .

    def handle_info({:tcp_closed, _socket}, state) do
    {:stop, :normal, %__MODULE__{state | socket: nil}}
    end
    def handle_info({:tcp, _socket, message}, state) do
    ConnectionManager.receive_message(state.manager, message, self())
    activate(self())
    {:noreply, state}
    end
    def handle_info(_unexpected_message, state), do: {:noreply, state}
    end

    View Slide

  76. defmodule ChatApp.Connection do
    # ..
    .

    def handle_info({:tcp_closed, _socket}, state) do
    {:stop, :normal, %__MODULE__{state | socket: nil}}
    end
    def handle_info({:tcp, _socket, message}, state) do
    ConnectionManager.receive_message(state.manager, message, self())
    activate(self())
    {:noreply, state}
    end
    def handle_info(_unexpected_message, state), do: {:noreply, state}
    end

    View Slide

  77. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def receive_message(manager \\ __MODULE__, message, from) do
    GenServer.cast(manager, {:receive_message, message, from})
    end
    def handle_cast({:receive_message, message, from}, state) do
    if state.mode == :host do
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} when pid != from -
    >

    Connection.queue_send(pid, message)
    _listener_or_from -
    >

    :ok
    end)
    end
    {name, content} = :erlang.binary_to_term(message)
    state.ui.show_chat_message(name, content)
    {:noreply, state}
    end
    end

    View Slide

  78. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def receive_message(manager \\ __MODULE__, message, from) do
    GenServer.cast(manager, {:receive_message, message, from})
    end
    def handle_cast({:receive_message, message, from}, state) do
    if state.mode == :host do
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} when pid != from -
    >

    Connection.queue_send(pid, message)
    _listener_or_from -
    >

    :ok
    end)
    end
    {name, content} = :erlang.binary_to_term(message)
    state.ui.show_chat_message(name, content)
    {:noreply, state}
    end
    end

    View Slide

  79. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def receive_message(manager \\ __MODULE__, message, from) do
    GenServer.cast(manager, {:receive_message, message, from})
    end
    def handle_cast({:receive_message, message, from}, state) do
    if state.mode == :host do
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} when pid != from -
    >

    Connection.queue_send(pid, message)
    _listener_or_from -
    >

    :ok
    end)
    end
    {name, content} = :erlang.binary_to_term(message)
    state.ui.show_chat_message(name, content)
    {:noreply, state}
    end
    end

    View Slide

  80. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def receive_message(manager \\ __MODULE__, message, from) do
    GenServer.cast(manager, {:receive_message, message, from})
    end
    def handle_cast({:receive_message, message, from}, state) do
    if state.mode == :host do
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} when pid != from -
    >

    Connection.queue_send(pid, message)
    _listener_or_from -
    >

    :ok
    end)
    end
    {name, content} = :erlang.binary_to_term(message)
    state.ui.show_chat_message(name, content)
    {:noreply, state}
    end
    end

    View Slide

  81. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def receive_message(manager \\ __MODULE__, message, from) do
    GenServer.cast(manager, {:receive_message, message, from})
    end
    def handle_cast({:receive_message, message, from}, state) do
    if state.mode == :host do
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} when pid != from -
    >

    Connection.queue_send(pid, message)
    _listener_or_from -
    >

    :ok
    end)
    end
    {name, content} = :erlang.binary_to_term(message)
    state.ui.show_chat_message(name, content)
    {:noreply, state}
    end
    end

    View Slide

  82. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp for_active_connections(func) do
    ConnectionSupervisor
    |> DynamicSupervisor.which_children()
    |> Enum.
    fi
    lter(fn {:unde
    fi
    ned, pid_or_restarting, :worker, _modules} -
    >

    is_pid(pid_or_restarting)
    end)
    |> Enum.each(func)
    end
    end

    View Slide

  83. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp for_active_connections(func) do
    ConnectionSupervisor
    |> DynamicSupervisor.which_children()
    |> Enum.
    fi
    lter(fn {:unde
    fi
    ned, pid_or_restarting, :worker, _modules} -
    >

    is_pid(pid_or_restarting)
    end)
    |> Enum.each(func)
    end
    end

    View Slide

  84. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp for_active_connections(func) do
    ConnectionSupervisor
    |> DynamicSupervisor.which_children()
    |> Enum.
    fi
    lter(fn {:unde
    fi
    ned, pid_or_restarting, :worker, _modules} -
    >

    is_pid(pid_or_restarting)
    end)
    |> Enum.each(func)
    end
    end

    View Slide

  85. Nesting Cast and Call
    • You can't nest a "call" to the same `GenServer` in another call (deadlock)


    • When nesting calls to other processes, consider the effect on timeouts


    • You can always nest a "cast" or `send/2` (`handle_info/2`)


    • Remember to report cast failure out-of-band when needed


    • Consider adding backpressure to protect the receiver from drowning

    View Slide

  86. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def send_to_all(manager \\ __MODULE__, message) do
    GenServer.call(manager, {:send_to_all, message})
    end
    def handle_call({:send_to_all, message}, _from, state) do
    result = queue_all_sends(message, state)
    {:reply, result, state}
    end
    end

    View Slide

  87. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def send_to_all(manager \\ __MODULE__, message) do
    GenServer.call(manager, {:send_to_all, message})
    end
    def handle_call({:send_to_all, message}, _from, state) do
    result = queue_all_sends(message, state)
    {:reply, result, state}
    end
    end

    View Slide

  88. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp queue_all_sends(message, %__MODULE__{mode: mode} = state)
    when mode in ~w[host client]a do
    ref = make_ref()
    prepared_message = :erlang.term_to_binary({state.name, message})
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} -
    >

    Connection.queue_send(pid, ref, prepared_message)
    _listener -
    >

    :ok
    end)
    {ref, state.name}
    end
    defp queue_all_sends(_message, _state), do: nil
    end

    View Slide

  89. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp queue_all_sends(message, %__MODULE__{mode: mode} = state)
    when mode in ~w[host client]a do
    ref = make_ref()
    prepared_message = :erlang.term_to_binary({state.name, message})
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} -
    >

    Connection.queue_send(pid, ref, prepared_message)
    _listener -
    >

    :ok
    end)
    {ref, state.name}
    end
    defp queue_all_sends(_message, _state), do: nil
    end

    View Slide

  90. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp queue_all_sends(message, %__MODULE__{mode: mode} = state)
    when mode in ~w[host client]a do
    ref = make_ref()
    prepared_message = :erlang.term_to_binary({state.name, message})
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} -
    >

    Connection.queue_send(pid, ref, prepared_message)
    _listener -
    >

    :ok
    end)
    {ref, state.name}
    end
    defp queue_all_sends(_message, _state), do: nil
    end

    View Slide

  91. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp queue_all_sends(message, %__MODULE__{mode: mode} = state)
    when mode in ~w[host client]a do
    ref = make_ref()
    prepared_message = :erlang.term_to_binary({state.name, message})
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} -
    >

    Connection.queue_send(pid, ref, prepared_message)
    _listener -
    >

    :ok
    end)
    {ref, state.name}
    end
    defp queue_all_sends(_message, _state), do: nil
    end

    View Slide

  92. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp queue_all_sends(message, %__MODULE__{mode: mode} = state)
    when mode in ~w[host client]a do
    ref = make_ref()
    prepared_message = :erlang.term_to_binary({state.name, message})
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} -
    >

    Connection.queue_send(pid, ref, prepared_message)
    _listener -
    >

    :ok
    end)
    {ref, state.name}
    end
    defp queue_all_sends(_message, _state), do: nil
    end

    View Slide

  93. defmodule ChatApp.ConnectionManager do
    # ..
    .

    defp queue_all_sends(message, %__MODULE__{mode: mode} = state)
    when mode in ~w[host client]a do
    ref = make_ref()
    prepared_message = :erlang.term_to_binary({state.name, message})
    for_active_connections(fn
    {:unde
    fi
    ned, pid, :worker, [Connection]} -
    >

    Connection.queue_send(pid, ref, prepared_message)
    _listener -
    >

    :ok
    end)
    {ref, state.name}
    end
    defp queue_all_sends(_message, _state), do: nil
    end

    View Slide

  94. defmodule ChatApp.Connection do
    # ..
    .

    def queue_send(connection, message), do: queue_send(connection, nil, message)
    def queue_send(connection, message_id, message) do
    GenServer.cast(connection, {:queue_send, message_id, message})
    end
    def handle_cast({:queue_send, message_id, message}, state) do
    case :gen_tcp.send(state.socket, message) do
    :ok -
    >

    :ok
    error -
    >

    if is_reference(message_id) do
    ConnectionManager.receive_send_error(state.manager, message_id, error)
    end
    end
    {:noreply, state}
    end
    end

    View Slide

  95. defmodule ChatApp.Connection do
    # ..
    .

    def queue_send(connection, message), do: queue_send(connection, nil, message)
    def queue_send(connection, message_id, message) do
    GenServer.cast(connection, {:queue_send, message_id, message})
    end
    def handle_cast({:queue_send, message_id, message}, state) do
    case :gen_tcp.send(state.socket, message) do
    :ok -
    >

    :ok
    error -
    >

    if is_reference(message_id) do
    ConnectionManager.receive_send_error(state.manager, message_id, error)
    end
    end
    {:noreply, state}
    end
    end

    View Slide

  96. defmodule ChatApp.Connection do
    # ..
    .

    def queue_send(connection, message), do: queue_send(connection, nil, message)
    def queue_send(connection, message_id, message) do
    GenServer.cast(connection, {:queue_send, message_id, message})
    end
    def handle_cast({:queue_send, message_id, message}, state) do
    case :gen_tcp.send(state.socket, message) do
    :ok -
    >

    :ok
    error -
    >

    if is_reference(message_id) do
    ConnectionManager.receive_send_error(state.manager, message_id, error)
    end
    end
    {:noreply, state}
    end
    end

    View Slide

  97. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def receive_send_error(manager \\ __MODULE__, message_id, error) do
    GenServer.cast(manager, {:receive_send_error, message_id, error})
    end
    def handle_cast({:receive_send_error, message_id, _error}, state) do
    state.ui.show_send_failure(message_id)
    {:noreply, state}
    end
    end

    View Slide

  98. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def receive_send_error(manager \\ __MODULE__, message_id, error) do
    GenServer.cast(manager, {:receive_send_error, message_id, error})
    end
    def handle_cast({:receive_send_error, message_id, _error}, state) do
    state.ui.show_send_failure(message_id)
    {:noreply, state}
    end
    end

    View Slide

  99. One More Thing…

    View Slide

  100. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def reset(manager \\ __MODULE__), do: GenServer.call(manager, :reset)
    def handle_call(:reset, _from, state) do
    queue_all_sends(:disconnected, state)
    for_active_connections(fn {:unde
    fi
    ned, pid, :worker, [module]} -
    >

    apply(module, :close, [pid])
    end)
    {:reply, :ok, %__MODULE__{}}
    end
    end

    View Slide

  101. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def reset(manager \\ __MODULE__), do: GenServer.call(manager, :reset)
    def handle_call(:reset, _from, state) do
    queue_all_sends(:disconnected, state)
    for_active_connections(fn {:unde
    fi
    ned, pid, :worker, [module]} -
    >

    apply(module, :close, [pid])
    end)
    {:reply, :ok, %__MODULE__{}}
    end
    end

    View Slide

  102. defmodule ChatApp.ConnectionManager do
    # ..
    .

    def reset(manager \\ __MODULE__), do: GenServer.call(manager, :reset)
    def handle_call(:reset, _from, state) do
    queue_all_sends(:disconnected, state)
    for_active_connections(fn {:unde
    fi
    ned, pid, :worker, [module]} -
    >

    apply(module, :close, [pid])
    end)
    {:reply, :ok, %__MODULE__{}}
    end
    end

    View Slide

  103. Let's Try It!
    I'm sure Brett won't mind


    if we give him a call…

    View Slide

  104. View Slide

  105. View Slide

  106. View Slide

  107. View Slide

  108. View Slide

  109. View Slide

  110. The User Interface

    View Slide

  111. The UI Problem
    • Will a Terminal interface work
    for this challenge?


    • What happens when a
    message arrives while we're
    typing?


    • This is another side of the
    multitasking challenge

    View Slide

  112. View Slide

  113. View Slide

  114. View Slide

  115. Cheating?
    • It is possible to use a Terminal


    • Terminals default to operation in "cooked" mode


    • This reads lines of input at a time and more


    • It's possible to switch to "raw" mode, say by shelling out to `stty`


    • In raw mode, you can read a character at a time


    • This allows keeping track of what has been entered so you can clear the
    screen and rerender as messages arrive

    View Slide

  116. Plan B

    View Slide

  117. defmodule ChatApp.GUI do
    use GenServer
    use Bitwise
    require Record
    alias ChatApp.ConnectionManager
    # constants from wx/include/wx.hr
    l

    @default 70
    @multiline 32
    @rich 32768
    @horizontal 4
    @vertical 8
    @left 16
    @right 32
    @up 64
    @down 128
    @all @left ||| @right ||| @up ||| @down
    @expand 8192
    @return_key 13
    # ..
    .

    end

    View Slide

  118. defmodule ChatApp.GUI do
    use GenServer
    use Bitwise
    require Record
    alias ChatApp.ConnectionManager
    # constants from wx/include/wx.hr
    l

    @default 70
    @multiline 32
    @rich 32768
    @horizontal 4
    @vertical 8
    @left 16
    @right 32
    @up 64
    @down 128
    @all @left ||| @right ||| @up ||| @down
    @expand 8192
    @return_key 13
    # ..
    .

    end

    View Slide

  119. defmodule ChatApp.GUI do
    # ..
    .

    defstruct window: nil
    ,

    chat: nil
    ,

    bold: nil
    ,

    italic: nil
    ,

    input: nil
    ,

    button: nil
    ,

    active_sends: Map.new()
    def start_link([]), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
    def init([]) do
    :timer.send_interval(5 * 60 * 1_000, :prune_active_sends)
    {:ok, %__MODULE__{}, {:continue, :show_gui}}
    end
    end

    View Slide

  120. defmodule ChatApp.GUI do
    # ..
    .

    defstruct window: nil
    ,

    chat: nil
    ,

    bold: nil
    ,

    italic: nil
    ,

    input: nil
    ,

    button: nil
    ,

    active_sends: Map.new()
    def start_link([]), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
    def init([]) do
    :timer.send_interval(5 * 60 * 1_000, :prune_active_sends)
    {:ok, %__MODULE__{}, {:continue, :show_gui}}
    end
    end

    View Slide

  121. defmodule ChatApp.GUI do
    # ..
    .

    defstruct window: nil
    ,

    chat: nil
    ,

    bold: nil
    ,

    italic: nil
    ,

    input: nil
    ,

    button: nil
    ,

    active_sends: Map.new()
    def start_link([]), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
    def init([]) do
    :timer.send_interval(5 * 60 * 1_000, :prune_active_sends)
    {:ok, %__MODULE__{}, {:continue, :show_gui}}
    end
    end

    View Slide

  122. defmodule ChatApp.GUI do
    # ..
    .

    defstruct window: nil
    ,

    chat: nil
    ,

    bold: nil
    ,

    italic: nil
    ,

    input: nil
    ,

    button: nil
    ,

    active_sends: Map.new()
    def start_link([]), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
    def init([]) do
    :timer.send_interval(5 * 60 * 1_000, :prune_active_sends)
    {:ok, %__MODULE__{}, {:continue, :show_gui}}
    end
    end

    View Slide

  123. defmodule ChatApp.GUI do
    # ..
    .

    def handle_continue(:show_gui, state) do
    wx = :wx.new()
    gui = :wx.batch(fn -> prepare_gui(wx) end)
    :wxWindow.show(gui.window)
    {
    :noreply
    ,

    %__MODULE__{
    stat
    e

    | window: gui.window
    ,

    chat: gui.chat
    ,

    bold: gui.bold
    ,

    italic: gui.italic
    ,

    input: gui.input
    ,

    button: gui.butto
    n

    }
    }
    end
    defp prepare_gui(wx) do
    w
    x

    |> build_gui()
    |> layout_gui()
    |> setup_events()
    end
    end

    View Slide

  124. defmodule ChatApp.GUI do
    # ..
    .

    def handle_continue(:show_gui, state) do
    wx = :wx.new()
    gui = :wx.batch(fn -> prepare_gui(wx) end)
    :wxWindow.show(gui.window)
    {
    :noreply
    ,

    %__MODULE__{
    stat
    e

    | window: gui.window
    ,

    chat: gui.chat
    ,

    bold: gui.bold
    ,

    italic: gui.italic
    ,

    input: gui.input
    ,

    button: gui.butto
    n

    }
    }
    end
    defp prepare_gui(wx) do
    w
    x

    |> build_gui()
    |> layout_gui()
    |> setup_events()
    end
    end

    View Slide

  125. defmodule ChatApp.GUI do
    # ..
    .

    def handle_continue(:show_gui, state) do
    wx = :wx.new()
    gui = :wx.batch(fn -> prepare_gui(wx) end)
    :wxWindow.show(gui.window)
    {
    :noreply
    ,

    %__MODULE__{
    stat
    e

    | window: gui.window
    ,

    chat: gui.chat
    ,

    bold: gui.bold
    ,

    italic: gui.italic
    ,

    input: gui.input
    ,

    button: gui.butto
    n

    }
    }
    end
    defp prepare_gui(wx) do
    w
    x

    |> build_gui()
    |> layout_gui()
    |> setup_events()
    end
    end

    View Slide

  126. defmodule ChatApp.GUI do
    # ..
    .

    defp build_gui(wx) do
    window = :wxFrame.new(wx, -1, "Chat", size: {800, 600})
    controls = :wxPanel.new(window)
    chat = :wxTextCtrl.new(controls, 1, style: @multiline ||| @rich)
    :wxTextCtrl.setEditable(chat, false)
    bold = :wxNORMAL_FONT |> :wxe_util.get_const() |> :wxFont.new()
    :wxFont.setWeight(bold, :wxe_util.get_const(:wxFONTWEIGHT_BOLD))
    italic = :wxe_util.get_const(:wxITALIC_FONT)
    c = "Commands:\n /listen PORT NAME\n /connect HOST PORT NAME\n /quit\n"
    append_text_with_font(chat, c, italic)
    form = :wxPanel.new(controls)
    input = :wxTextCtrl.new(form, 2, style: @default)
    button = :wxButton.new(form, 3, label: "Send")
    %{
    window: window
    ,

    controls: controls
    ,

    chat: chat
    ,

    bold: bold
    ,

    italic: italic
    ,

    form: form
    ,

    input: input
    ,

    button: butto
    n

    }
    end
    end

    View Slide

  127. defmodule ChatApp.GUI do
    # ..
    .

    defp build_gui(wx) do
    window = :wxFrame.new(wx, -1, "Chat", size: {800, 600})
    controls = :wxPanel.new(window)
    chat = :wxTextCtrl.new(controls, 1, style: @multiline ||| @rich)
    :wxTextCtrl.setEditable(chat, false)
    bold = :wxNORMAL_FONT |> :wxe_util.get_const() |> :wxFont.new()
    :wxFont.setWeight(bold, :wxe_util.get_const(:wxFONTWEIGHT_BOLD))
    italic = :wxe_util.get_const(:wxITALIC_FONT)
    c = "Commands:\n /listen PORT NAME\n /connect HOST PORT NAME\n /quit\n"
    append_text_with_font(chat, c, italic)
    form = :wxPanel.new(controls)
    input = :wxTextCtrl.new(form, 2, style: @default)
    button = :wxButton.new(form, 3, label: "Send")
    %{
    window: window
    ,

    controls: controls
    ,

    chat: chat
    ,

    bold: bold
    ,

    italic: italic
    ,

    form: form
    ,

    input: input
    ,

    button: butto
    n

    }
    end
    end

    View Slide

  128. defmodule ChatApp.GUI do
    # ..
    .

    defp build_gui(wx) do
    window = :wxFrame.new(wx, -1, "Chat", size: {800, 600})
    controls = :wxPanel.new(window)
    chat = :wxTextCtrl.new(controls, 1, style: @multiline ||| @rich)
    :wxTextCtrl.setEditable(chat, false)
    bold = :wxNORMAL_FONT |> :wxe_util.get_const() |> :wxFont.new()
    :wxFont.setWeight(bold, :wxe_util.get_const(:wxFONTWEIGHT_BOLD))
    italic = :wxe_util.get_const(:wxITALIC_FONT)
    c = "Commands:\n /listen PORT NAME\n /connect HOST PORT NAME\n /quit\n"
    append_text_with_font(chat, c, italic)
    form = :wxPanel.new(controls)
    input = :wxTextCtrl.new(form, 2, style: @default)
    button = :wxButton.new(form, 3, label: "Send")
    %{
    window: window
    ,

    controls: controls
    ,

    chat: chat
    ,

    bold: bold
    ,

    italic: italic
    ,

    form: form
    ,

    input: input
    ,

    button: butto
    n

    }
    end
    end

    View Slide

  129. defmodule ChatApp.GUI do
    # ..
    .

    defp append_text(ctrl, text) do
    :wxTextCtrl.appendText(ctrl, text)
    end
    defp append_text_with_font(ctrl, text, font) do
    style = :wxTextCtrl.getDefaultStyle(ctrl)
    :wxTextAttr.setFont(style, font)
    :wxTextCtrl.setDefaultStyle(ctrl, style)
    append_text(ctrl, text)
    :wxTextAttr.setFont(style, :wxe_util.get_const(:wxNORMAL_FONT))
    :wxTextCtrl.setDefaultStyle(ctrl, style)
    end
    end

    View Slide

  130. defmodule ChatApp.GUI do
    # ..
    .

    defp append_text(ctrl, text) do
    :wxTextCtrl.appendText(ctrl, text)
    end
    defp append_text_with_font(ctrl, text, font) do
    style = :wxTextCtrl.getDefaultStyle(ctrl)
    :wxTextAttr.setFont(style, font)
    :wxTextCtrl.setDefaultStyle(ctrl, style)
    append_text(ctrl, text)
    :wxTextAttr.setFont(style, :wxe_util.get_const(:wxNORMAL_FONT))
    :wxTextCtrl.setDefaultStyle(ctrl, style)
    end
    end

    View Slide

  131. defmodule ChatApp.GUI do
    # ..
    .

    defp layout_gui(gui) do
    window_sizer = :wxBoxSizer.new(@vertical)
    :wxSizer.add(
    window_sizer
    ,

    gui.controls
    ,

    border: 4
    ,

    proportion: 1
    ,

    fl
    ag: @expand ||| @all
    )
    controls_sizer = :wxBoxSizer.new(@vertical)
    :wxSizer.add(controls_sizer, gui.chat, proportion: 1,
    fl
    ag: @expand)
    :wxSizer.addSpacer(controls_sizer, 4)
    :wxSizer.add(controls_sizer, gui.form, proportion: 0,
    fl
    ag: @expand)
    form_sizer = :wxBoxSizer.new(@horizontal)
    :wxSizer.add(form_sizer, gui.input, proportion: 1,
    fl
    ag: @expand)
    :wxSizer.addSpacer(controls_sizer, 4)
    :wxSizer.add(form_sizer, gui.button)
    :wxWindow.setSizer(gui.form, form_sizer)
    :wxWindow.setSizer(gui.controls, controls_sizer)
    :wxWindow.setSizer(gui.window, window_sizer)
    gu
    i

    end
    end

    View Slide

  132. defmodule ChatApp.GUI do
    # ..
    .

    defp layout_gui(gui) do
    window_sizer = :wxBoxSizer.new(@vertical)
    :wxSizer.add(
    window_sizer
    ,

    gui.controls
    ,

    border: 4
    ,

    proportion: 1
    ,

    fl
    ag: @expand ||| @all
    )
    controls_sizer = :wxBoxSizer.new(@vertical)
    :wxSizer.add(controls_sizer, gui.chat, proportion: 1,
    fl
    ag: @expand)
    :wxSizer.addSpacer(controls_sizer, 4)
    :wxSizer.add(controls_sizer, gui.form, proportion: 0,
    fl
    ag: @expand)
    form_sizer = :wxBoxSizer.new(@horizontal)
    :wxSizer.add(form_sizer, gui.input, proportion: 1,
    fl
    ag: @expand)
    :wxSizer.addSpacer(controls_sizer, 4)
    :wxSizer.add(form_sizer, gui.button)
    :wxWindow.setSizer(gui.form, form_sizer)
    :wxWindow.setSizer(gui.controls, controls_sizer)
    :wxWindow.setSizer(gui.window, window_sizer)
    gu
    i

    end
    end

    View Slide

  133. defmodule ChatApp.GUI do
    # ..
    .

    defp setup_events(gui) do
    :wxWindow.setFocus(gui.input)
    :wxEvtHandler.connect(gui.window, :close_window)
    :wxEvtHandler.connect(gui.input, :key_down, skip: true)
    :wxEvtHandler.connect(gui.button, :command_button_clicked)
    gu
    i

    end
    end

    View Slide

  134. defmodule ChatApp.GUI do
    # ..
    .

    defp setup_events(gui) do
    :wxWindow.setFocus(gui.input)
    :wxEvtHandler.connect(gui.window, :close_window)
    :wxEvtHandler.connect(gui.input, :key_down, skip: true)
    :wxEvtHandler.connect(gui.button, :command_button_clicked)
    gu
    i

    end
    end

    View Slide

  135. defmodule ChatApp.GUI do
    # ..
    .

    defp setup_events(gui) do
    :wxWindow.setFocus(gui.input)
    :wxEvtHandler.connect(gui.window, :close_window)
    :wxEvtHandler.connect(gui.input, :key_down, skip: true)
    :wxEvtHandler.connect(gui.button, :command_button_clicked)
    gu
    i

    end
    end

    View Slide

  136. Records

    View Slide

  137. Records

    View Slide

  138. Records

    View Slide

  139. Records

    View Slide

  140. Records

    View Slide

  141. Records

    View Slide

  142. defmodule ChatApp.GUI do
    # ..
    .

    Record.extract_all(from_lib: "wx/include/wx.hrl")
    |> Enum.map(fn {name,
    fi
    elds} -> Record.defrecordp(name,
    fi
    elds) end)
    def handle_info(
    wx(
    id: _id
    ,

    obj: window
    ,

    userData: _userData
    ,

    event: wxClose(type: :close_window)
    )
    ,

    %__MODULE__{window: window} = stat
    e

    ) do
    quit(state)
    {:noreply, state}
    end
    defp quit(state) do
    ConnectionManager.reset()
    :wxWindow.destroy(state.window)
    :wx.destroy()
    System.stop(0)
    end
    end

    View Slide

  143. defmodule ChatApp.GUI do
    # ..
    .

    Record.extract_all(from_lib: "wx/include/wx.hrl")
    |> Enum.map(fn {name,
    fi
    elds} -> Record.defrecordp(name,
    fi
    elds) end)
    def handle_info(
    wx(
    id: _id
    ,

    obj: window
    ,

    userData: _userData
    ,

    event: wxClose(type: :close_window)
    )
    ,

    %__MODULE__{window: window} = stat
    e

    ) do
    quit(state)
    {:noreply, state}
    end
    defp quit(state) do
    ConnectionManager.reset()
    :wxWindow.destroy(state.window)
    :wx.destroy()
    System.stop(0)
    end
    end

    View Slide

  144. defmodule ChatApp.GUI do
    # ..
    .

    Record.extract_all(from_lib: "wx/include/wx.hrl")
    |> Enum.map(fn {name,
    fi
    elds} -> Record.defrecordp(name,
    fi
    elds) end)
    def handle_info(
    wx(
    id: _id
    ,

    obj: window
    ,

    userData: _userData
    ,

    event: wxClose(type: :close_window)
    )
    ,

    %__MODULE__{window: window} = stat
    e

    ) do
    quit(state)
    {:noreply, state}
    end
    defp quit(state) do
    ConnectionManager.reset()
    :wxWindow.destroy(state.window)
    :wx.destroy()
    System.stop(0)
    end
    end

    View Slide

  145. defmodule ChatApp.GUI do
    # ..
    .

    Record.extract_all(from_lib: "wx/include/wx.hrl")
    |> Enum.map(fn {name,
    fi
    elds} -> Record.defrecordp(name,
    fi
    elds) end)
    def handle_info(
    wx(
    id: _id
    ,

    obj: window
    ,

    userData: _userData
    ,

    event: wxClose(type: :close_window)
    )
    ,

    %__MODULE__{window: window} = stat
    e

    ) do
    quit(state)
    {:noreply, state}
    end
    defp quit(state) do
    ConnectionManager.reset()
    :wxWindow.destroy(state.window)
    :wx.destroy()
    System.stop(0)
    end
    end

    View Slide

  146. defmodule ChatApp.GUI do
    # ..
    .

    def handle_info(
    wx(
    id: _id
    ,

    obj: input
    ,

    userData: _userData
    ,

    event:
    wxKey(
    type: :key_down
    ,

    x: _x
    ,

    y: _y
    ,

    keyCode: @return_key
    ,

    controlDown: false
    ,

    shiftDown: false
    ,

    altDown: false
    ,

    metaDown: false
    ,

    uniChar: _uniChar
    ,

    rawCode: _rawCode
    ,

    rawFlags: _rawFlags
    )
    )
    ,

    %__MODULE__{input: input} = stat
    e

    ) do
    new_active_sends = process_input(state)
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    end
    end

    View Slide

  147. defmodule ChatApp.GUI do
    # ..
    .

    def handle_info(
    wx(
    id: _id
    ,

    obj: input
    ,

    userData: _userData
    ,

    event:
    wxKey(
    type: :key_down
    ,

    x: _x
    ,

    y: _y
    ,

    keyCode: @return_key
    ,

    controlDown: false
    ,

    shiftDown: false
    ,

    altDown: false
    ,

    metaDown: false
    ,

    uniChar: _uniChar
    ,

    rawCode: _rawCode
    ,

    rawFlags: _rawFlags
    )
    )
    ,

    %__MODULE__{input: input} = stat
    e

    ) do
    new_active_sends = process_input(state)
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    end
    end

    View Slide

  148. defmodule ChatApp.GUI do
    # ..
    .

    def handle_info(
    wx(
    id: _id
    ,

    obj: input
    ,

    userData: _userData
    ,

    event:
    wxKey(
    type: :key_down
    ,

    x: _x
    ,

    y: _y
    ,

    keyCode: @return_key
    ,

    controlDown: false
    ,

    shiftDown: false
    ,

    altDown: false
    ,

    metaDown: false
    ,

    uniChar: _uniChar
    ,

    rawCode: _rawCode
    ,

    rawFlags: _rawFlags
    )
    )
    ,

    %__MODULE__{input: input} = stat
    e

    ) do
    new_active_sends = process_input(state)
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    end
    end

    View Slide

  149. defmodule ChatApp.GUI do
    # ..
    .

    def handle_info(
    wx(
    id: _id
    ,

    obj: button
    ,

    userData: _userData
    ,

    event:
    wxCommand(
    type: :command_button_clicked
    ,

    cmdString: _cmdString
    ,

    commandInt: _commandInt
    ,

    extraLong: _extraLong
    )
    )
    ,

    %__MODULE__{button: button} = stat
    e

    ) do
    new_active_sends = process_input(state)
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    end
    end

    View Slide

  150. defmodule ChatApp.GUI do
    # ..
    .

    def handle_info(
    wx(
    id: _id
    ,

    obj: button
    ,

    userData: _userData
    ,

    event:
    wxCommand(
    type: :command_button_clicked
    ,

    cmdString: _cmdString
    ,

    commandInt: _commandInt
    ,

    extraLong: _extraLong
    )
    )
    ,

    %__MODULE__{button: button} = stat
    e

    ) do
    new_active_sends = process_input(state)
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    end
    end

    View Slide

  151. defmodule ChatApp.GUI do
    # ..
    .

    def handle_info(
    wx(
    id: _id
    ,

    obj: button
    ,

    userData: _userData
    ,

    event:
    wxCommand(
    type: :command_button_clicked
    ,

    cmdString: _cmdString
    ,

    commandInt: _commandInt
    ,

    extraLong: _extraLong
    )
    )
    ,

    %__MODULE__{button: button} = stat
    e

    ) do
    new_active_sends = process_input(state)
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    end
    end

    View Slide

  152. defmodule ChatApp.GUI do
    # ..
    .

    defp process_input(state) do
    new_active_sends
    =

    case state.input |> :wxTextCtrl.getValue() |> to_string() do
    "/" <> command -
    >

    process_command(command, state)
    state.active_send
    s

    message when byte_size(message) > 0 -
    >

    case ConnectionManager.send_to_all(message) do
    {ref, name} -
    >

    append_message(name, message, state)
    now = System.monotonic_time(:second)
    Map.put(state.active_sends, ref, {message, now})
    nil -
    >

    state.active_send
    s

    end
    "" -
    >

    state.active_send
    s

    end
    :wxTextCtrl.clear(state.input)
    new_active_send
    s

    end
    end

    View Slide

  153. defmodule ChatApp.GUI do
    # ..
    .

    defp process_input(state) do
    new_active_sends
    =

    case state.input |> :wxTextCtrl.getValue() |> to_string() do
    "/" <> command -
    >

    process_command(command, state)
    state.active_send
    s

    message when byte_size(message) > 0 -
    >

    case ConnectionManager.send_to_all(message) do
    {ref, name} -
    >

    append_message(name, message, state)
    now = System.monotonic_time(:second)
    Map.put(state.active_sends, ref, {message, now})
    nil -
    >

    state.active_send
    s

    end
    "" -
    >

    state.active_send
    s

    end
    :wxTextCtrl.clear(state.input)
    new_active_send
    s

    end
    end

    View Slide

  154. defmodule ChatApp.GUI do
    # ..
    .

    defp process_input(state) do
    new_active_sends
    =

    case state.input |> :wxTextCtrl.getValue() |> to_string() do
    "/" <> command -
    >

    process_command(command, state)
    state.active_send
    s

    message when byte_size(message) > 0 -
    >

    case ConnectionManager.send_to_all(message) do
    {ref, name} -
    >

    append_message(name, message, state)
    now = System.monotonic_time(:second)
    Map.put(state.active_sends, ref, {message, now})
    nil -
    >

    state.active_send
    s

    end
    "" -
    >

    state.active_send
    s

    end
    :wxTextCtrl.clear(state.input)
    new_active_send
    s

    end
    end

    View Slide

  155. defmodule ChatApp.GUI do
    # ..
    .

    defp process_input(state) do
    new_active_sends
    =

    case state.input |> :wxTextCtrl.getValue() |> to_string() do
    "/" <> command -
    >

    process_command(command, state)
    state.active_send
    s

    message when byte_size(message) > 0 -
    >

    case ConnectionManager.send_to_all(message) do
    {ref, name} -
    >

    append_message(name, message, state)
    now = System.monotonic_time(:second)
    Map.put(state.active_sends, ref, {message, now})
    nil -
    >

    state.active_send
    s

    end
    "" -
    >

    state.active_send
    s

    end
    :wxTextCtrl.clear(state.input)
    new_active_send
    s

    end
    end

    View Slide

  156. defmodule ChatApp.GUI do
    # ..
    .

    defp process_input(state) do
    new_active_sends
    =

    case state.input |> :wxTextCtrl.getValue() |> to_string() do
    "/" <> command -
    >

    process_command(command, state)
    state.active_send
    s

    message when byte_size(message) > 0 -
    >

    case ConnectionManager.send_to_all(message) do
    {ref, name} -
    >

    append_message(name, message, state)
    now = System.monotonic_time(:second)
    Map.put(state.active_sends, ref, {message, now})
    nil -
    >

    state.active_send
    s

    end
    "" -
    >

    state.active_send
    s

    end
    :wxTextCtrl.clear(state.input)
    new_active_send
    s

    end
    end

    View Slide

  157. defmodule ChatApp.GUI do
    # ..
    .

    defp process_input(state) do
    new_active_sends
    =

    case state.input |> :wxTextCtrl.getValue() |> to_string() do
    "/" <> command -
    >

    process_command(command, state)
    state.active_send
    s

    message when byte_size(message) > 0 -
    >

    case ConnectionManager.send_to_all(message) do
    {ref, name} -
    >

    append_message(name, message, state)
    now = System.monotonic_time(:second)
    Map.put(state.active_sends, ref, {message, now})
    nil -
    >

    state.active_send
    s

    end
    "" -
    >

    state.active_send
    s

    end
    :wxTextCtrl.clear(state.input)
    new_active_send
    s

    end
    end

    View Slide

  158. defmodule ChatApp.GUI do
    # ..
    .

    defp process_input(state) do
    new_active_sends
    =

    case state.input |> :wxTextCtrl.getValue() |> to_string() do
    "/" <> command -
    >

    process_command(command, state)
    state.active_send
    s

    message when byte_size(message) > 0 -
    >

    case ConnectionManager.send_to_all(message) do
    {ref, name} -
    >

    append_message(name, message, state)
    now = System.monotonic_time(:second)
    Map.put(state.active_sends, ref, {message, now})
    nil -
    >

    state.active_send
    s

    end
    "" -
    >

    state.active_send
    s

    end
    :wxTextCtrl.clear(state.input)
    new_active_send
    s

    end
    end

    View Slide

  159. defmodule ChatApp.GUI do
    # ..
    .

    defp process_input(state) do
    new_active_sends
    =

    case state.input |> :wxTextCtrl.getValue() |> to_string() do
    "/" <> command -
    >

    process_command(command, state)
    state.active_send
    s

    message when byte_size(message) > 0 -
    >

    case ConnectionManager.send_to_all(message) do
    {ref, name} -
    >

    append_message(name, message, state)
    now = System.monotonic_time(:second)
    Map.put(state.active_sends, ref, {message, now})
    nil -
    >

    state.active_send
    s

    end
    "" -
    >

    state.active_send
    s

    end
    :wxTextCtrl.clear(state.input)
    new_active_send
    s

    end
    end

    View Slide

  160. defmodule ChatApp.GUI do
    # ..
    .

    defp append_message(name, message, state) do
    append_text_with_font(state.chat, name, state.bold)
    append_text(state.chat, ": #{message}\n")
    end
    end

    View Slide

  161. defmodule ChatApp.GUI do
    # ..
    .

    defp append_message(name, message, state) do
    append_text_with_font(state.chat, name, state.bold)
    append_text(state.chat, ": #{message}\n")
    end
    end

    View Slide

  162. defmodule ChatApp.GUI do
    # ..
    .

    defp append_message(name, message, state) do
    append_text_with_font(state.chat, name, state.bold)
    append_text(state.chat, ": #{message}\n")
    end
    end

    View Slide

  163. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("listen" <> args, state) do
    case Regex.named_captures(~r{\A\s+(?\d+)\s+(?\S.*)\z}, args) do
    %{"port" => port, "name" => name} -
    >

    case ConnectionManager.listen(String.to_integer(port), name) do
    :ok -
    >

    append_text_with_font(state.chat, "Listening\n", state.italic)
    _error -
    >

    append_text_with_font(
    state.chat
    ,

    "Error: listening failed\n"
    ,

    state.itali
    c

    )
    end
    nil -
    >

    append_text_with_font(
    state.chat
    ,

    "Usage: /listen PORT NAME\n"
    ,

    state.itali
    c

    )
    end
    end
    end

    View Slide

  164. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("listen" <> args, state) do
    case Regex.named_captures(~r{\A\s+(?\d+)\s+(?\S.*)\z}, args) do
    %{"port" => port, "name" => name} -
    >

    case ConnectionManager.listen(String.to_integer(port), name) do
    :ok -
    >

    append_text_with_font(state.chat, "Listening\n", state.italic)
    _error -
    >

    append_text_with_font(
    state.chat
    ,

    "Error: listening failed\n"
    ,

    state.itali
    c

    )
    end
    nil -
    >

    append_text_with_font(
    state.chat
    ,

    "Usage: /listen PORT NAME\n"
    ,

    state.itali
    c

    )
    end
    end
    end

    View Slide

  165. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("listen" <> args, state) do
    case Regex.named_captures(~r{\A\s+(?\d+)\s+(?\S.*)\z}, args) do
    %{"port" => port, "name" => name} -
    >

    case ConnectionManager.listen(String.to_integer(port), name) do
    :ok -
    >

    append_text_with_font(state.chat, "Listening\n", state.italic)
    _error -
    >

    append_text_with_font(
    state.chat
    ,

    "Error: listening failed\n"
    ,

    state.itali
    c

    )
    end
    nil -
    >

    append_text_with_font(
    state.chat
    ,

    "Usage: /listen PORT NAME\n"
    ,

    state.itali
    c

    )
    end
    end
    end

    View Slide

  166. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("listen" <> args, state) do
    case Regex.named_captures(~r{\A\s+(?\d+)\s+(?\S.*)\z}, args) do
    %{"port" => port, "name" => name} -
    >

    case ConnectionManager.listen(String.to_integer(port), name) do
    :ok -
    >

    append_text_with_font(state.chat, "Listening\n", state.italic)
    _error -
    >

    append_text_with_font(
    state.chat
    ,

    "Error: listening failed\n"
    ,

    state.itali
    c

    )
    end
    nil -
    >

    append_text_with_font(
    state.chat
    ,

    "Usage: /listen PORT NAME\n"
    ,

    state.itali
    c

    )
    end
    end
    end

    View Slide

  167. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("connect" <> args, state) do
    case Regex.named_captures(
    ~r{\A\s+(?\S+)\s+(?\d+)\s+(?\S.*)\z}
    ,

    arg
    s

    ) do
    %{"host" => host, "port" => port, "name" => name} -
    >

    case ConnectionManager.connect(host, String.to_integer(port), name) do
    :ok -
    >

    append_text_with_font(state.chat, "Connected\n", state.italic)
    _error -
    >

    append_text_with_font(
    state.chat
    ,

    "Error: connecting failed\n"
    ,

    state.itali
    c

    )
    end
    nil -
    >

    append_text_with_font(
    state.chat
    ,

    "Usage: /connect HOST PORT NAME\n"
    ,

    state.itali
    c

    )
    end
    end
    end

    View Slide

  168. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("connect" <> args, state) do
    case Regex.named_captures(
    ~r{\A\s+(?\S+)\s+(?\d+)\s+(?\S.*)\z}
    ,

    arg
    s

    ) do
    %{"host" => host, "port" => port, "name" => name} -
    >

    case ConnectionManager.connect(host, String.to_integer(port), name) do
    :ok -
    >

    append_text_with_font(state.chat, "Connected\n", state.italic)
    _error -
    >

    append_text_with_font(
    state.chat
    ,

    "Error: connecting failed\n"
    ,

    state.itali
    c

    )
    end
    nil -
    >

    append_text_with_font(
    state.chat
    ,

    "Usage: /connect HOST PORT NAME\n"
    ,

    state.itali
    c

    )
    end
    end
    end

    View Slide

  169. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("connect" <> args, state) do
    case Regex.named_captures(
    ~r{\A\s+(?\S+)\s+(?\d+)\s+(?\S.*)\z}
    ,

    arg
    s

    ) do
    %{"host" => host, "port" => port, "name" => name} -
    >

    case ConnectionManager.connect(host, String.to_integer(port), name) do
    :ok -
    >

    append_text_with_font(state.chat, "Connected\n", state.italic)
    _error -
    >

    append_text_with_font(
    state.chat
    ,

    "Error: connecting failed\n"
    ,

    state.itali
    c

    )
    end
    nil -
    >

    append_text_with_font(
    state.chat
    ,

    "Usage: /connect HOST PORT NAME\n"
    ,

    state.itali
    c

    )
    end
    end
    end

    View Slide

  170. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("connect" <> args, state) do
    case Regex.named_captures(
    ~r{\A\s+(?\S+)\s+(?\d+)\s+(?\S.*)\z}
    ,

    arg
    s

    ) do
    %{"host" => host, "port" => port, "name" => name} -
    >

    case ConnectionManager.connect(host, String.to_integer(port), name) do
    :ok -
    >

    append_text_with_font(state.chat, "Connected\n", state.italic)
    _error -
    >

    append_text_with_font(
    state.chat
    ,

    "Error: connecting failed\n"
    ,

    state.itali
    c

    )
    end
    nil -
    >

    append_text_with_font(
    state.chat
    ,

    "Usage: /connect HOST PORT NAME\n"
    ,

    state.itali
    c

    )
    end
    end
    end

    View Slide

  171. Not Very DRY
    Beware the trap!

    View Slide

  172. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("quit", state), do: quit(state)
    defp process_command(_unknown_command, state) do
    append_text_with_font(
    state.chat
    ,

    "Error: unknown command\n"
    ,

    state.itali
    c

    )
    end
    end

    View Slide

  173. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("quit", state), do: quit(state)
    defp process_command(_unknown_command, state) do
    append_text_with_font(
    state.chat
    ,

    "Error: unknown command\n"
    ,

    state.itali
    c

    )
    end
    end

    View Slide

  174. defmodule ChatApp.GUI do
    # ..
    .

    defp process_command("quit", state), do: quit(state)
    defp process_command(_unknown_command, state) do
    append_text_with_font(
    state.chat
    ,

    "Error: unknown command\n"
    ,

    state.itali
    c

    )
    end
    end

    View Slide

  175. defmodule ChatApp.GUI do
    # ..
    .

    def show_send_failure(ref) do
    GenServer.cast(__MODULE__, {:show_send_failure, ref})
    end
    def handle_cast({:show_send_failure, ref}, state) do
    case Map.pop(state.active_sends, ref) do
    {{message, _timestamp}, new_active_sends} -
    >

    append_text_with_font(
    state.chat
    ,

    "The following message was not received by all participants: " <
    >

    "#{message}\n"
    ,

    state.itali
    c

    )
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    {nil, _active_sends} -
    >

    {:noreply, state}
    end
    end
    end

    View Slide

  176. defmodule ChatApp.GUI do
    # ..
    .

    def show_send_failure(ref) do
    GenServer.cast(__MODULE__, {:show_send_failure, ref})
    end
    def handle_cast({:show_send_failure, ref}, state) do
    case Map.pop(state.active_sends, ref) do
    {{message, _timestamp}, new_active_sends} -
    >

    append_text_with_font(
    state.chat
    ,

    "The following message was not received by all participants: " <
    >

    "#{message}\n"
    ,

    state.itali
    c

    )
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    {nil, _active_sends} -
    >

    {:noreply, state}
    end
    end
    end

    View Slide

  177. defmodule ChatApp.GUI do
    # ..
    .

    def show_send_failure(ref) do
    GenServer.cast(__MODULE__, {:show_send_failure, ref})
    end
    def handle_cast({:show_send_failure, ref}, state) do
    case Map.pop(state.active_sends, ref) do
    {{message, _timestamp}, new_active_sends} -
    >

    append_text_with_font(
    state.chat
    ,

    "The following message was not received by all participants: " <
    >

    "#{message}\n"
    ,

    state.itali
    c

    )
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    {nil, _active_sends} -
    >

    {:noreply, state}
    end
    end
    end

    View Slide

  178. defmodule ChatApp.GUI do
    # ..
    .

    def handle_info(:prune_active_sends, state) do
    expired = System.monotonic_time(:second) - 5 * 60
    new_active_sends
    =

    state.active_send
    s

    |> Enum.reject(fn {_ref, {_message, timestamp}} -
    >

    timestamp < expire
    d

    end)
    |> Map.new()
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    end
    def handle_info(_message, state), do: {:noreply, state}
    end

    View Slide

  179. defmodule ChatApp.GUI do
    # ..
    .

    def handle_info(:prune_active_sends, state) do
    expired = System.monotonic_time(:second) - 5 * 60
    new_active_sends
    =

    state.active_send
    s

    |> Enum.reject(fn {_ref, {_message, timestamp}} -
    >

    timestamp < expire
    d

    end)
    |> Map.new()
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    end
    def handle_info(_message, state), do: {:noreply, state}
    end

    View Slide

  180. defmodule ChatApp.GUI do
    # ..
    .

    def handle_info(:prune_active_sends, state) do
    expired = System.monotonic_time(:second) - 5 * 60
    new_active_sends
    =

    state.active_send
    s

    |> Enum.reject(fn {_ref, {_message, timestamp}} -
    >

    timestamp < expire
    d

    end)
    |> Map.new()
    {:noreply, %__MODULE__{state | active_sends: new_active_sends}}
    end
    def handle_info(_message, state), do: {:noreply, state}
    end

    View Slide

  181. Pardon My Mess
    I'm a Web Developer,


    not a GUI expert!

    View Slide

  182. defmodule ChatApp.GUI do
    # ..
    .

    def show_chat_message(name, content) do
    GenServer.cast(__MODULE__, {:show_chat_message, name, content})
    end
    def handle_cast({:show_chat_message, name, action}, state)
    when is_atom(action) do
    append_text_with_font(state.chat, "#{name} #{action}\n", state.italic)
    {:noreply, state}
    end
    def handle_cast({:show_chat_message, name, content}, state) do
    append_message(name, content, state)
    {:noreply, state}
    end
    end

    View Slide

  183. defmodule ChatApp.GUI do
    # ..
    .

    def show_chat_message(name, content) do
    GenServer.cast(__MODULE__, {:show_chat_message, name, content})
    end
    def handle_cast({:show_chat_message, name, action}, state)
    when is_atom(action) do
    append_text_with_font(state.chat, "#{name} #{action}\n", state.italic)
    {:noreply, state}
    end
    def handle_cast({:show_chat_message, name, content}, state) do
    append_message(name, content, state)
    {:noreply, state}
    end
    end

    View Slide

  184. defmodule ChatApp.GUI do
    # ..
    .

    def show_chat_message(name, content) do
    GenServer.cast(__MODULE__, {:show_chat_message, name, content})
    end
    def handle_cast({:show_chat_message, name, action}, state)
    when is_atom(action) do
    append_text_with_font(state.chat, "#{name} #{action}\n", state.italic)
    {:noreply, state}
    end
    def handle_cast({:show_chat_message, name, content}, state) do
    append_message(name, content, state)
    {:noreply, state}
    end
    end

    View Slide

  185. defmodule ChatApp.GUI do
    # ..
    .

    def show_chat_message(name, content) do
    GenServer.cast(__MODULE__, {:show_chat_message, name, content})
    end
    def handle_cast({:show_chat_message, name, action}, state)
    when is_atom(action) do
    append_text_with_font(state.chat, "#{name} #{action}\n", state.italic)
    {:noreply, state}
    end
    def handle_cast({:show_chat_message, name, content}, state) do
    append_message(name, content, state)
    {:noreply, state}
    end
    end

    View Slide

  186. Shall We Try it Out?
    Let's phone a friend…

    View Slide

  187. Big Finish Time
    Let's phone two friends!

    View Slide

  188. View Slide

  189. View Slide

  190. View Slide

  191. Thank You

    View Slide

  192. Questions?
    • These slides


    • https://speakerdeck.com/jeg2/how-to-level-up-in-elixir


    • The Code


    • https://github.com/JEG2/chat_app


    • Forum Posts


    • https://elixirforum.com/t/becoming-an-intermediate-elixir-developer/
    37991


    • https://elixirforum.com/t/how-to-become-a-senior-in-elixir/40430

    View Slide