I’ve spent a lot of time arguing against event systems. I don’t argue because they’re bad; compared to batch processing or shared-state imperative concurrency, event systems (actors, vats, channels, etc.) solve many problems and are relatively easy to reason about. Rather, I argue against event systems because we can do even better. But my writings are scattered across many forums.
This article shall provide a place to consolidate my arguments against event-based modeling and control of systems. It does not promote any specific alternative – I leave that to other articles.
By events, I include commands, messages, procedure calls, other conceptually `instantaneous` values in the system that independently effect or report changes in state. Consequently, I consider message passing systems to be event processing models. In general, I assume that events are processed by single threaded event loops without shared state, though there might be multiple such threads in an event driven system. I.e. I’m claiming a position even against sane event systems – like actors model, pi calculus, CSP, and E-language vats. (The event calculus is closer to temporal logic; most of my complaints do not apply to event calculus, despite the name.)
By their essential nature as instantaneous observations and effects, events introduce much `accidental complexity` in our systems. As with most systemic accidental complexity, this is not obvious until you’ve experienced something better. Here are examples of accidental complexity and the atrocities people inflict upon themselves with event systems:
- We very often want views of the current state of a system. Events represent or effect changes in state. By using events, we commit ourselves to using event accumulators – explicit state – for this common computational task.
- We very often want to combine events from multiple sensors and sources in order to represent composite events. This event fusion requires much explicit state.
- Often we wish to fuse views of overlapping subsystems, or overlapping perspectives. When views are event streams, there is a troublesome condition of implicit event replication: e.g. a single button press might be observed in both event streams, perhaps with translations, with no general means to recognize it as a common event that should only be processed once. Inability to effectively work with overlapping data-model abstractions severely hinders compositional reasoning and reuse of event streams.
- Events require much implicit state in their communication models – i.e. queues, events in transit. Each event carries a snapshot of the past into the future, with implicit time passing between send and receive.
- Events handle inconsistently for non-linear processing. In particular, when we split an event into two events, we must order them – introducing a notion of `time` (this event before that one) within what was a conceptually instantaneous action.
- State in event systems is fragile. A lost, reordered, or replicated event can affect (and potentially corrupt) the whole future of a program. This is true of both explicit and implicit state in event systems. Minor bugs in code (e.g. missing an event to release resources) or hiccups in scheduling or communication tend to propagate.
- Abstractions in event systems are fragile. Event fusion is highly sensitive to local, arbitrary ordering decisions for merging events that otherwise appear simultaneous. Even in a deterministic system, it is difficult for two observers to achieve consistent views of complex event streams without sharing actual implementation code. Consequently, event systems are difficult to reason about in the presence of open extension. In general, we cannot robustly compose views specified in event systems.
- Event ontologies are not compositional. Sending two events in most event systems lacks the same meaning or performance (atomicity, progress, efficiency, latency) as sending one composite event. Providing events from two sources is not the same as fusing events then sending the composite. Developers are faced with tidal pressures towards both larger events (for efficiency, atomic updates, data fusion) and smaller events (for simplicity, modularity). Ontologies grow in ad-hoc manners, hindering development of generic composition operators. This issue can be mitigated by code-distribution abstractions (batching, promise pipelining, mobile agents, scripting).
- Event systems work harder. Events are the “changes in state” that someone else (e.g. a framework developer) considered important enough to report. And that someone else often lacks the foresight to accommodate our needs. We invariably need a another perspective. For event systems, achieving another perspective requires three complex tasks: fusing events into state, detecting patterns in prior state relative to the current state after each event, generate a new series of events that we consider important. By comparison, state transform models and filters (functions, queries) are relatively simple to express, reason about, and automatically optimize.
- Event systems lack generic resilience. Developers have built patterns for resilient event systems – timeouts, retries, watchdogs, command patterns. Unfortunately, these patterns require careful attention to the specific events model – i.e. where retries are safe, where losing an event is safe, which subsystems can be restarted. Many of these recovery models are not compositional – e.g. timeouts are not compositional because we need to understand the timeouts of each subsystem. Many are non-deterministic and work poorly if replicated across views. By comparison, state models can generically achieve simple resilience properties like eventual consistency and snapshot consistency. Often, developers in event systems will eventually reinvent a more declarative, RESTful approach – but typically in a non-composable, non-reentrant, glitchy, lossy, inefficient, buggy, high-overhead manner (like observer patterns).
The complexity of event systems and eventful state models is indexed on permutations of expressions, whereas the complexity of declarative systems is indexed on combinations of expressions. Event systems, in general, are exponentially more complex than declarative systems to achieve the same goal. And most of that complexity is non-essential, accidental. All but the most trivial of event systems quickly grow so complex and difficult to reason about that there are no obvious bugs or inefficiencies.
The usual knee-jerk response to eliminating events is:
What about button presses?!
To which my knee-jerk answer is: this is an almost stupidly trivial example to defend event systems, and it is trivial to model button presses – or any other real-world event (ignoring eventful abstractions you invent for yourself after assuming an eventful paradigm) – in terms of live state. A button-press is observable as an up state, followed by a down state, followed by an up state – each state with a positive, computable duration. (And by computable I mean a subset of the rational subset of real numbers. Even one picosecond is okay.) Ah, but I’ve learned that playing roshambo with knee-jerk responses is painful and unsatisfactory for all parties.
It seems, after wading through confusion and miscommunication, the actual concern is that: “but querying state with events is lossy! I’ll lose button presses! and it’s really frustrating to lose button presses, e.g. when working with a joystick.” When said like that, it becomes obvious that the problem is a bad assumption: state can only be queried by events.
With an “eventless” model, we cannot use events to query for state! (And I mean that in the trivial sense; to do so would be a contradiction in terms.) We must instead use state to query for state. State is continuous. Our queries are continuous. You might understand such queries in terms of “subscriptions”. The FRP and synchronous reactive communities understand them in terms of signals. The database community might grok them in terms of streaming temporal data. In any case, if our queries are properly continuous, we will not miss any intermediate states. We’ll observe button presses even if they last only one picosecond, no polling.
The representation of buttons as signal sources (representing current state) offers several advantages over eventful buttons. In particular, it is easy to combine observations of multiple buttons, or combine observation of a button with some other ad-hoc condition (
when Foo and Button Pressed ...). Such expressions on multiple sources are robust against order of expression, and semantically stateless, making it very easy to achieve consistency between observers. It is easy to combine overlapping views that share the same button, with resilient and compositional safety net for consistency (snapshot consistency, or eventual consistency, depending on how much intermediate processing is contributing to the views). A late arriver observing the system can view the state for all the buttons, not just the ones most recently manipulated, without explicit ad-hoc support. Eventless observations on state are commutative, idempotent, concurrent, continuous, and declarative.
When we do need events, we can recognize them as differences in state. This is valuable because we often seek complex ad-hoc events, just as we seek complex and ad-hoc conditions. (Usefully, some models may allow stateless event detection – i.e. by comparing present and future so we don’t depend on any estimate of the past.)
Of course, we do need to use declarative state models. The imperative state models with which you are most familiar are generally designed for event-driven manipulation and observation (mutual exclusion, isolation, transactions). Declarative state models must handle concurrent, continuous influence. A button press might influence state for one picosecond, but that’s potentially enough to change it.