sasa1977 / exactor

Helpers for simpler implementation of GenServer based processes

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

how does it differ from elixir agent

allyraza opened this issue · comments

What I see lot of similarities with Elixir's builtin Agent, can you shed some light if there any notable differences.

Thank you

This is a very good question! While at the surface both seem similar, I regard them as quite different, and the difference between the two amounts to the difference between Agents and GenServer, so let me discuss that first.

Agents are abstractions around the state. You have a process which holds on to some state, and you can modify/query this state from different processes, and these operations are serialized (since an Agent is a process). Arguably, Agent is a bit easier to write than a GenServer process, but you lose some features. For example, with GenServer you can handle plain messages (e.g. :EXIT and :DOWN messages), you can defer sending the response for later, or even delegate the response to another process. You can provide hibernate and idle timeout hints, and in each callback you can decide to stop the process. There is a multicall/multicast support, which simplifies interaction with processes on multiple nodes. None of these things are possible with Agents, though you can work around some of the limitations.

Finally, it's worth mentioning that unlike GenServers, Agents in fact expose the process state. Any client has the freedom of arbitrarily changing the state of the agent, which breaks the encapsulation. To reduce this issue, you should wrap your Agent in a dedicated module, and make the clients use the functions from that module, thus making them unaware of the underlying implementation.

In contrast, GenServer is an abstraction around a long-running process which manages some state, and can react to various messages. Unlike Agents, GenServers encapsulate state. When you implement a GenServer you provide a set of messages which can be sent to the process. You can then interact with the process only by sending these messages. Consequently, clients can do only what the server permits them (actually you can circumvent this through :sys.get_state and :sys.replace_state, but these are explicitly documented as debugging helpers only). GenServer processes require a bit more code, but you get more flexibility.

ExActor is a set of helper macros that aim to simplify some typical scenarios I have seen in practice, as explained here. A module powered by ExActor implements the GenServer behaviour, so you can freely mix the code with standard GenServer code. For example, you can return standard GenServer tuples from ExActor macros such as defcall and defcast. You can also combine ExActor macros with plain GenServer callbacks in the same module, though it's usually not needed, since most GenServer features are already supported.

As mentioned in Readme, I don't advise using ExActor until you're confident about GenServer. ExActor is essentially a boilerplate removal helper, and not an abstraction. It doesn't try to hide the underlying GenServer, so it's critical that you understand how it works. Once you're familiar with GenServer, you should be able to understand how ExActor macros translate to the plain GenServer callbacks. I've taken a lot of effort to reduce the magical parts, so in many cases macros basically generate two functions (interface and a callback clause), without adding anything else. The only implicit "magic" I can think of is the usage of process dictionary to support global timeouts/hibernations.

I should note that I myself have mixed feelings about ExActor. The problem is that the core helper macros (e.g. defcall) basically generate two functions, and this has limitations, for example when you want to have a finer control around the pattern matching. I've worked a lot to support such cases, providing explicit control for pattern matches on both sides (client and the server), and helpers for generating private functions, but the resulting code then might become noisy to the point where ExActor doesn't reduce the noise, possibly even increases it.

I'd also like to say that I'm personally not a fan of Agents, because I don't think they help a lot, and regard them as a limited version of GenServers, so I prefer the more powerful and flexible abstraction. So when it comes to stateful processes, the only question for me is should I use GenServer or ExActor.

With time, I'm getting more inclined to suggest using plain GenServer. ExActor is a non-standard extension, so the code will be less approachable to others. Due to macros, there are some implicit code transformations happening, so it might not be obvious what goes on in the generated code. In contrast, GenServer is idiomatic in both Elixir and Erlang world, not very hard to learn, and much more explicit. So despite it being somewhat noisy, I regard it as the best choice for most types of stateful server processes.

I can vouch for exactor, I use it everywhere and find it dramatically simplifies my cofe

Thanks for the explanation!

Thank you for writing in response to my question. I may have asked the wrong question, I saw the name exactor poped up in one of my projects I immediately rushed to github to look at it when I noticed the macros I figure maybe it is somewhat like agents. But thanks to your explanation it is more clear now that its just a syntactic sugar and takes care some of the house keeping just to make it easy to work with genservers.

I know that agents are a mare abstraction around genserver to manage state, I don't know if you know much about where the idea of agents came from agents were originally inspired by clojure's state agents so I use them when I need something to be around between processes, if I need fine control I will just implement genserver behaviour.

Thank you once again

If you intend to share some state between multiple processes, and that state will be often read, ETS tables might work better. With ETS tables, you can have simultaneous reads from multiple processes, something not possible when the state is kept in a single process. This can boost performance significantly, because readers don't block each other at all. You'll probably still need a single writer process through which state modifications are serialized. If the the modification doesn't involve reading the previous version of the state, you can even get away without the writer process. The same holds if the state is an integer which you need to increment/decrement atomically.

Anyway, glad I cleared things up. I think there are some overlapping concerns between Agents and GenServers (and consequently ExActor), so this was good occasion for me to rant a bit about this topic :-)

agree ETS is much better option when you have multiple processes consuming or updating state, when I said need something be around in multiple process I meant I stick something into the agent in one process and read it somewhere else possibly different process.

If it’s just about storing some data so it can be read later by someone else, I’d probably use ETS, but maybe that’s just my Erlang background speaking :-) Agent can work for this as well, and maybe such simple case is a good example of where it can be useful.

On 08 Jan 2016, at 19:15, Ally Raza notifications@github.com wrote:

agree ETS is much better option when you have multiple processes consuming or updating state, when I said need something be around in multiple process I meant I stick something into the agent in one process and read it somewhere else possibly different process.


Reply to this email directly or view it on GitHub #13 (comment).