12 Functional design for concurrent systems
This chapter addresses the core challenge of taming complexity in concurrent software by designing systems as sets of isolated, message-driven components. It advocates the actor mindset—components share nothing and communicate via messages—and reframes those messages as reactive streams that can be transformed and composed functionally. By focusing on what components do (their behaviors) rather than what data they expose, the design eliminates shared mutable state and the synchronization it demands, making systems easier to reason about, safer, and more scalable.
The chapter develops a practical, typed-actor toolkit and demonstrates it through a small bookmark-processing service. It introduces sources, sinks, and general actors, then builds a stream pipeline with range-like, pipeable operators (transform, filter, join). Reactive streams are treated as monads: transform lifts functions over streams, values constructs streams from constants, and join flattens streams of streams. Errors are handled functionally by converting exceptions into values (expected) and composing with mbind, preserving error information throughout. To support replies while keeping the pipeline decoupled, messages are wrapped in a with_client context that carries the client handle alongside evolving payloads; lifting utilities adapt ordinary functions to operate transparently over this nested, monadic structure.
While promoting immutability, the chapter also shows where stateful actors are appropriate (for example, join keeping active sources, or throttling to mitigate abusive clients). Because each actor processes messages serially and shares no state, such mutability requires no locks. Crucially, the functional, message-centric design decouples logic from infrastructure: moving from single-threaded to multithreaded or distributed execution chiefly changes message delivery (queues, scheduling, serialization), not the pipeline itself. The result is a composable, testable, and resilient approach to concurrent and distributed system design grounded in actors and reactive streams.
We can’t set the attributes of real-life objects. We can send them messages and let the objects react to those messages.
An actor is an isolated component that can receive and send messages. It processes the messages serially; for each message, it can change its state or behavior, or it can send a new message to another actor in the system.
You’ll use simplified typed actors that don’t need to care about who sends the message to whom: that will be left to an external controller.
The service actor listens for client connections and for their messages. This part is hidden from the rest of the program, which just knows a stream of strings is arriving from somewhere.
You capture the shared pointer to the session object in the lambda that will process when new data arrives from the client. The session object will be kept alive as long as the client keeps the connection alive.
Unlike the continuation monad, which calls the continuation function only once, reactive streams can have an arbitrary number of values. The continuation function is called for each new value that arrives.
The sink actor calls a function for each message it receives. It emits nothing. You can use it to print out all the messages you receive to std::cerr or do something more advanced such as saving the messages to a file or a database.
Similar to ranges, transform on reactive streams applies the given transformation function on each message it receives, and it sends the function result to the next actor.
Summary
- Most C++ programmers write procedural code. I recommend reading Object Thinking by David West (Microsoft Press, 2004) to help you start writing better object-oriented code. It’s beneficial even when you’re doing functional programming.
- Humans tend to achieve grand things when they talk to one another. We don’t have a shared mind, but the ability to communicate helps us achieve complex goals. This is the reasoning that led to the invention of the actor model.
- Monads can cooperate well. You shouldn’t be afraid to stack them on each other.
- You can implement transformations for reactive streams similar to those for input ranges. You can’t implement things such as sorting on them, because for sorting you need random access to all elements, and you don’t even know how many elements a reactive stream will have—they’re potentially infinite.
- Just like futures, common implementations of reactive streams aren’t limited to sending values; they can also send special messages such as “stream ended.” This can be useful for more efficient memory handling: you can destroy a stream when you know it won’t send you any more messages.
FAQ
What core problem does this chapter tackle in concurrent software?
It addresses the explosion of complexity that arises from entangled components and shared mutable state, especially under concurrency. The chapter’s answer is to split systems into isolated components (actors) that communicate by sending messages, which reduces synchronization needs and makes behavior easier to reason about and compose.How does the chapter’s actor model differ from a traditional actor framework?
The chapter uses simplified, typed actors: each actor receives one message type and emits one (not necessarily the same) message type. Routing and scheduling are delegated to an external controller, which enables functional composition. Traditional actors often support multiple message types and choose recipients themselves; the chapter’s design prioritizes composability over feature breadth.What are sources, sinks, and general actors?
- Sources only emit messages (no input).- Sinks only receive messages (no output).
- General actors both receive and emit. Actors expose a way to set a message handler and a process function for incoming messages, keep their internal state private, and avoid shared data, thereby eliminating the need for locks.
Functional Programming in C++ ebook for free