Dec Code Story

Chapter #11 – Declarative Concurrency

software transactional memory, actor model, functional reactive programming, dataflow

11.0  Prologue

Imperative concurrency requires the programmer to manually coordinate access to shared mutable state using locks, mutexes, and condition variables - a recipe for deadlocks, race conditions, and subtle ordering bugs. Declarative approaches attack the root cause: either eliminate shared mutable state entirely (actors, FRP) or make concurrent state changes composable and atomic (STM). The result is concurrent code that is easier to reason about and test.

11.1  Software Transactional Memory

Software transactional memory (STM) borrows the database concept of transactions for in-memory state. A transaction reads and writes shared variables (TVars in Haskell); if no conflict is detected at commit time, the changes are applied atomically. If a conflict occurs, the transaction retries automatically. -- Haskell: STM with Control.Concurrent.STM import Control.Concurrent.STM type Account = TVar Int transfer :: Account -> Account -> Int -> STM () transfer from to amount = do fromBal <- readTVar from if fromBal < amount then retry -- block until balance changes, then retry else do modifyTVar from (subtract amount) modifyTVar to (+ amount) -- atomically runs the STM action as a single atomic transaction main :: IO () main = do alice <- newTVarIO 100 bob <- newTVarIO 50 atomically (transfer alice bob 30) -- alice = 70, bob = 80; no locks required STM composes: two STM actions joined with >> or do-notation form a single larger atomic action. This composability is impossible with locks - combining two lock-based operations safely requires knowing and acquiring both locks in the right order.

11.2  Actor Model

In the actor model, the unit of concurrency is an actor: an isolated entity with its own private state that communicates only by sending and receiving immutable messages. Because no state is shared, races and locks do not arise. Actors may run on different threads, processes, or machines. -- Erlang / Elixir: processes are actors; spawn + send + receive defmodule Counter do def start(n), do: spawn(fn -> loop(n) end) defp loop(n) do receive do {:increment, caller} -> send(caller, {:ok, n + 1}) loop(n + 1) {:get, caller} -> send(caller, {:value, n}) loop(n) end end end pid = Counter.start(0) send(pid, {:increment, self()}) receive do {:ok, v} -> IO.puts(v) end # 1 -- Rust: channel-based message passing (actor-like) use std::sync::mpsc; let (tx, rx) = mpsc::channel(); std::thread::spawn(move || { tx.send(42).unwrap(); }); let value = rx.recv().unwrap(); // 42 Erlang/OTP and Elixir build fault-tolerant systems from millions of lightweight actors. Rust’s ownership model enforces actor-like isolation at compile time: a value moved into a thread cannot be accessed from the sender, eliminating data races without a runtime.

11.3  Functional Reactive Programming

Functional reactive programming (FRP) models time-varying values and event streams as first-class values. Instead of callbacks that mutate state, FRP expresses how outputs depend on inputs declaratively: the runtime propagates changes automatically when inputs update. -- Haskell (reflex-frp): Behavior and Event are the two FRP primitives -- Behavior t a - a value of type a that changes continuously over time t -- Event t a - a discrete occurrence of type a at a point in time -- Example concept (simplified pseudocode): -- mousePos :: Behavior t (Int, Int) -- current mouse position -- clicks :: Event t () -- fires on each click -- clickPos :: Event t (Int, Int) -- clickPos = current mousePos <@ clicks -- sample mousePos at each click -- JavaScript (RxJS): Observable streams (FRP-inspired) import { fromEvent, merge } from 'rxjs'; import { map, scan, startWith } from 'rxjs/operators'; const clicks$ = fromEvent(document, 'click'); const count$ = clicks$.pipe( scan(n => n + 1, 0), startWith(0) ); count$.subscribe(n => console.log('clicks:', n)); -- Elm: pure FRP architecture (Model-Update-View) -- update : Msg -> Model -> Model (pure function, no side effects) -- view : Model -> Html Msg (pure function, renders current state) FRP eliminates callback hell and mutable event-handler state. The Elm architecture (Model-Update-View) is a widely adopted, FRP-inspired pattern for building UIs with pure functions.

11.4  Dataflow and Streaming

Dataflow programming expresses a computation as a directed graph of nodes connected by data channels. Nodes fire when their inputs are available; the scheduler drives execution. This is the model behind many stream-processing systems (Apache Kafka Streams, Flink, Spark Streaming). -- Python (asyncio): async generators as lazy dataflow pipelines import asyncio async def source(items): for item in items: await asyncio.sleep(0) # yield control yield item async def transform(stream): async for item in stream: yield item * 2 async def sink(stream): async for item in stream: print(item) asyncio.run(sink(transform(source([1, 2, 3])))) # prints: 2, 4, 6 -- Haskell (pipes / conduit): composable streaming with backpressure -- Producer -> Pipe -> Consumer -- Each stage runs only as fast as the downstream consumer demands -- Apache Flink (Java/Scala): distributed dataflow -- DataStream<String> stream = env.addSource(kafkaSource); -- stream.filter(s -> s.startsWith("ERR")) -- .keyBy(s -> s.split(":")[0]) -- .window(TumblingEventTimeWindows.of(Time.minutes(1))) -- .sum("count") -- .addSink(dbSink); Dataflow and streaming systems express concurrency as data movement through a graph rather than as threads sharing state. Backpressure - the ability of a slow consumer to signal a fast producer to slow down - is a key property that prevents unbounded buffering.

11.5  Epilogue

Declarative concurrency replaces the lock-and-mutate model with safer, more composable abstractions: STM for shared state, actors for isolated state, FRP for time-varying values, and dataflow for pipeline parallelism. Each approach eliminates a different class of concurrency bug by construction. The final chapter examines how declarative languages express queries and logical rules over structured data.

11.6  References

Haskell STM - Hackage
Erlang Processes - Official Docs
Elm Architecture - Official Guide
RxJS - Overview