Is it possible to have multi states in genserver?

Hey everyone,

As we all know GenServer exposes one immutable state to the business context, thus any minimal update to the state will mean creating new state.

Why people don’t recommend using the process dict(only when it makes sense) global state when even Ecto uses it?

Something like this will be cool:
handle_info(call, state_1, state_2, state_n)

I don’t understand why beam doesn’t even support mutable state in genserver, because the state is being modified only sequentially by the application.

There are a few questions here:

It’s not like you’re supposed to never use the process dictionary. It has it’s uses, which is why it exists in the first place, but you should consider the tradeoffs in terms of how things work if you want to split logic up between processes and also the plain indirection of using the process dictionary to keep state around instead of explicitly passing data from a to b. If you can get away by not using the process dict it’s likely that you shouldn’t use it.

There’s mutable state on the beam, but it’s ETS. In quite a lot of cases you don’t really need to reach for it though, because the beam doesn’t handle state changes by copying the complete state you have. If you use maps only the data, which changed will be updated and everything, which is still the same will use the stuff already in memory. If you update the head of a list it’ll simply link to the old tail of the list. Big binaries are actually moved off the process heap to a shared location for improved performance and less copying. On the surface you can expect it to behave like the whole state was copied, but the beam does do optimizations where it can.

With what I stated above this is hardly different than e.g.:
handle_info(call, %{state_1: state_1, state_2: state_2, state_n: state_n})

7 Likes

Thank you for your quick response,
ETS doesn’t make sense at all to my application, and I will not call it a mutable state as it requires moving the data around.

Currently am using tuple state which hold
{ {counter, {list, list}} , map}

do you recommend moving all of this to map?

handle_info(call, %{state_1: state_1, state_2: state_2, state_n: state_n})

This will still require updating the map using Map.put/update/merge/etc,
instead of dealing with each state independently.

What’s wrong with:

def handle_call(..., state) do
  state1 = some_fun(state[:state1])
  {:reply, ..., %{state | state1: state1})`
end

I think “independently” is arguable in your statement above, because where exactly are the “individual” states going to come from? How would you determine which one to pass to which handler?

1 Like

In my opinion a map that holds the state is more clear than a tuple.

The maps fields are named while the fields in the tuple are positional and you have to remember the positions. When you swap them around your application might fail miserable and in an undebugable way. If though you get some names wrong, the error messages will be clear (most of the time).

5 Likes

All handlers should have access to all the pointers that get the states. even better if we let the compiler define the pointer(s) which should be passed to a given handler by introducing new types.

@spec handle_call(..., state_5)
def handle_call(..., state_5) do
  state_5 = some_fun(state_5)
  {:reply, ..., state_5)`
end

As you might see, the business context doesn’t had to interact with state_1…4, and those states never get touched.

Large parts of the “new state” may in fact be the “old state” - thanks to immutability, persistent data structures are possible, so that “minimal update” will likely not create a whole new copy of the state.

3 Likes

From How Elixir Manages Memory?

So you see the areas we set them all, nice and easy, updating a value at this size of array (up to size 100) only involves setting 3 tuples, which on the BEAM is FAST rather than needing to copy everything . :slight_smile:

Would you elaborate more about this?

It’s easiest to see with lists. If I have:

a = [1,2,3]
b = [5] ++ a

the contents of List A do not need to be copied. Rather, the tail of B just points to A. Immutability means that you can always safely point to existing values and rely on them not to change. Similarly with a map if you have:

foo = %{key1: value1, key2: value2}
Map.put(foo, :key2, some_other_value)

Some internal parts of the map may be copied but not the actual values themselves. Consequently if you have several states you’re tracking in a map, if you want to put a change to them, the newly returned map can still continue to point to the other parts of the map that aren’t new, and no meaningful copying occurs. For more on this you’ll want to read about persistent data structures.

2 Likes

Thank you Ben, I will move the states from nested tuple to map.

Same thing applies to tuple :wink:

Still reasonable to move from map in order to make the code more clear, but at that size, they’re probably pretty close to equivalent in terms of efficiency & copying. (Depends a bit on the inner tuples, but not something likely to impact performance in a real way.)

1 Like

yeah, but I liked tuples because I noticed tuple pattern matching is way faster than pattern matching map.

ahah… didn’t even think of that possibility…

Ehhhh not really. It’s faster, but rarely in a way that matters. Particularly with small maps both are nanosecond length operations, it’s just not worth picking that one part to hyper optimize.

1 Like