6
\$\begingroup\$

It seems odd that state must be passed around all the time. Can't State module hold itself, so I could do something like

state = %State{size: size}
case state.is_full? do
    ...

Here state is always passed. I suppose this is how it is in FP?

defmodule Chatter.Room do
  use GenServer  

  defmodule State do
    defstruct size: 3, players: []

    def is_full?(state) do
      Enum.count(state.players) >= state.size
    end

    def add_player(state, player) do
      case State.is_full?(state) do
        true -> {:room_is_full, state}
        _    -> {:ok, append_player(state, player)}
      end
    end

    defp append_player(state, player) do
      %{state | players: state.players ++ [player]}
    end
  end

  def open(size) do
    {:ok, pid} = GenServer.start_link(__MODULE__, %State{size: size})
    pid
  end

  def join(room, user) do
    GenServer.call(room, {:join, user})
  end

  def players(room) do
    GenServer.call(room, :players)
  end


  def init(state) do
    {:ok, state}
  end

  def handle_call(:players, _from, state) do
    {:reply, state.players, state}
  end

  def handle_call({:join, player}, _from, state) do
    case State.add_player(state, player) do
      {:ok, state} -> {:reply, :ok, state}
      {error, state} -> {:reply, error, state}
    end
  end
end
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Short answer -- yes, data including state is immutable.

In traditional functional programming idiom data is immutable. With this in mind, GenServer stores the current state internally. When you implement the callback functions (handle_call, handle_cast, ...) the current state is passed to your callback function. You then return a new state which then replaces the existing state inside the GenServer. Depending on the data structure used for maintaining your state, only a portion of the new data may be "deeply" copied.

If I relate this directly to your question, I would not implement state as you have. The GenServer wraps the state for you. A more erlang/elixirish outline would be something like:

defmodule ModuleName do
  # api functions 
  # place calls to cast, call, here
  def is_full?(server) do
    GenServer.call(server, {:check_full})
  end

  # callbacks
  # match ":check_full" call
  def handle_call({:check_full}, _from, state) do
    # perform your full check actions here
  end

  # match an ":add_user" call
  def handle_call({:add_user, {user_data}}, _from, state) do
    # perform your add_user actions here
  end

  # catch all
  def handle_call(request, _from, state) do
    # do something here, if you want, or omit as suits your style
  end
end

Certainly, you can also use guards instead of matching or use one giant handle_call function with a case or cond to separate out the pieces. The method used above is clean and easy to understand.

I may have missed the point of the question --- the short answer was, yes, we pass the state around. The longer answer was the GenServer can keep track of it for you.

\$\endgroup\$
1
  • \$\begingroup\$ I would like to note that in this example and the question, the state is managed via blocking calls. Cast/handle_cast is non-blocking and is definitely useful in many cases. \$\endgroup\$
    – uDude
    Jan 5, 2016 at 23:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.