🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Project: Chat Server

A chat server is a GenServer that keeps a room of members and a list of messages. Members join by name, and every message goes to the room's shared list.

Members join by name

Joining adds a name to the member list. The room can list its members at any time.

defmodule Room do
  use GenServer
  def start_link(_arg) do
    GenServer.start_link(__MODULE__, %{members: [], messages: []})
  end
  def init(state), do: {:ok, state}
  def handle_call({:join, name}, {pid, _tag}, state) do
    members = state.members ++ [{name, pid}]
    {:reply, :ok, %{state | members: members}}
  end
  def handle_call({:say, user, text}, {sender, _tag}, state) do
    for {_name, pid} <- state.members, pid != sender do
      send(pid, {:chat, user, text})
    end
    messages = state.messages ++ [{user, text}]
    {:reply, :ok, %{state | messages: messages}}
  end
  def handle_call(:members, _from, state) do
    names = Enum.map(state.members, fn {name, _pid} -> name end)
    {:reply, names, state}
  end
  def handle_call(:transcript, _from, state) do
    {:reply, state.messages, state}
  end
  def handle_call({:leave, name}, _from, state) do
    members = Enum.reject(state.members, fn {member, _pid} -> member == name end)
    {:reply, :ok, %{state | members: members}}
  end
end
{:ok, room} = Room.start_link([])
GenServer.call(room, {:join, "Ana"})
GenServer.call(room, {:join, "Bo"})
IO.inspect(GenServer.call(room, :members))
GenServer.stop(room)

Messages land in the transcript in order

Each message is added to the end of the list. The transcript therefore reads in send order.

defmodule Room do
  use GenServer
  def start_link(_arg) do
    GenServer.start_link(__MODULE__, %{members: [], messages: []})
  end
  def init(state), do: {:ok, state}
  def handle_call({:join, name}, {pid, _tag}, state) do
    members = state.members ++ [{name, pid}]
    {:reply, :ok, %{state | members: members}}
  end
  def handle_call({:say, user, text}, {sender, _tag}, state) do
    for {_name, pid} <- state.members, pid != sender do
      send(pid, {:chat, user, text})
    end
    messages = state.messages ++ [{user, text}]
    {:reply, :ok, %{state | messages: messages}}
  end
  def handle_call(:members, _from, state) do
    names = Enum.map(state.members, fn {name, _pid} -> name end)
    {:reply, names, state}
  end
  def handle_call(:transcript, _from, state) do
    {:reply, state.messages, state}
  end
  def handle_call({:leave, name}, _from, state) do
    members = Enum.reject(state.members, fn {member, _pid} -> member == name end)
    {:reply, :ok, %{state | members: members}}
  end
end
{:ok, room} = Room.start_link([])
GenServer.call(room, {:join, "Ana"})
GenServer.call(room, {:join, "Bo"})
GenServer.call(room, {:say, "Ana", "Hello"})
GenServer.call(room, {:say, "Bo", "Hi, Ana"})
IO.inspect(GenServer.call(room, :transcript))
GenServer.stop(room)

Leaving removes a member

Leaving filters the name out of the member list. The other members stay.

defmodule Room do
  use GenServer
  def start_link(_arg) do
    GenServer.start_link(__MODULE__, %{members: [], messages: []})
  end
  def init(state), do: {:ok, state}
  def handle_call({:join, name}, {pid, _tag}, state) do
    members = state.members ++ [{name, pid}]
    {:reply, :ok, %{state | members: members}}
  end
  def handle_call({:say, user, text}, {sender, _tag}, state) do
    for {_name, pid} <- state.members, pid != sender do
      send(pid, {:chat, user, text})
    end
    messages = state.messages ++ [{user, text}]
    {:reply, :ok, %{state | messages: messages}}
  end
  def handle_call(:members, _from, state) do
    names = Enum.map(state.members, fn {name, _pid} -> name end)
    {:reply, names, state}
  end
  def handle_call(:transcript, _from, state) do
    {:reply, state.messages, state}
  end
  def handle_call({:leave, name}, _from, state) do
    members = Enum.reject(state.members, fn {member, _pid} -> member == name end)
    {:reply, :ok, %{state | members: members}}
  end
end
{:ok, room} = Room.start_link([])
GenServer.call(room, {:join, "Ana"})
GenServer.call(room, {:join, "Bo"})
GenServer.call(room, {:join, "Cid"})
GenServer.call(room, {:leave, "Bo"})
IO.inspect(GenServer.call(room, :members))
GenServer.stop(room)

Example

defmodule Room do
  use GenServer
  def start_link(_arg) do
    GenServer.start_link(__MODULE__, %{members: [], messages: []})
  end
  def init(state), do: {:ok, state}
  def handle_call({:join, name}, {pid, _tag}, state) do
    members = state.members ++ [{name, pid}]
    {:reply, :ok, %{state | members: members}}
  end
  def handle_call({:say, user, text}, {sender, _tag}, state) do
    for {_name, pid} <- state.members, pid != sender do
      send(pid, {:chat, user, text})
    end
    messages = state.messages ++ [{user, text}]
    {:reply, :ok, %{state | messages: messages}}
  end
  def handle_call(:members, _from, state) do
    names = Enum.map(state.members, fn {name, _pid} -> name end)
    {:reply, names, state}
  end
  def handle_call(:transcript, _from, state) do
    {:reply, state.messages, state}
  end
  def handle_call({:leave, name}, _from, state) do
    members = Enum.reject(state.members, fn {member, _pid} -> member == name end)
    {:reply, :ok, %{state | members: members}}
  end
end
{:ok, room} = Room.start_link([])
GenServer.call(room, {:join, "Ana"})
parent = self()
listener = spawn(fn ->
  collect = fn collect, acc ->
    receive do
      {:chat, user, text} -> collect.(collect, acc ++ [{user, text}])
      {:report, from} ->
        send(from, {:log, acc})
        collect.(collect, acc)
      :stop -> :ok
    end
  end
  GenServer.call(room, {:join, "Bo"})
  send(parent, :joined)
  collect.(collect, [])
end)
receive do :joined -> :ok end
GenServer.call(room, {:say, "Ana", "Hello, room"})
GenServer.call(room, {:say, "Bo", "Hi, Ana"})
IO.inspect(GenServer.call(room, :members))
IO.inspect(GenServer.call(room, :transcript))
send(listener, {:report, self()})
receive do {:log, log} -> IO.inspect(log) end
send(listener, :stop)
GenServer.stop(room)

Key points

  • Join: a user joins the room by name.
  • Broadcast: a message is sent to every other member of the room.
  • Transcript: the room keeps messages in the order they were sent.
  • Leave: a user who leaves is removed from the member list.

In this example, Ana joins the room, then a second process joins as Bo and waits for the join to be confirmed. Ana and Bo each send one message, the room lists its members and its transcript, and Bo's own delivery log matches the transcript exactly. The listener is stopped and the room is stopped at the end.

Note

the room prints names and message text only, never process identifiers.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

Ana says Hello and then Bo says Hi, Ana. What does the room return for its transcript?