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?