Messaging as a Noun: Kay, Milner, and dao.stream
Ask a room of programmers what the most important property of object-oriented programming is, and you will hear encapsulation, inheritance, polymorphism, or "modeling the real world with classes." Ask the person who coined the term, and you get a different answer.
Alan Kay has repeatedly expressed regret that the name "objects" led people to focus on the lesser idea. The big idea, in his telling, was messaging: independent, encapsulated entities, like biological cells or computers on a network, that interact only by sending messages to one another. The objects were the cells. The messages were the point.
This post has two parts. The first follows Kay's idea through three stages: how mainstream OOP lost it, how the pi-calculus formalized it, and how dao.stream takes it one step further than either. The second tests that claim against the formalism: how much of the pi-calculus does dao.stream implement, what does an interpreter like yin.vm add, and what is deliberately left out?
1. How OOP Languages Lost the Message
C++, Java, and C# kept the vocabulary of messaging while quietly replacing its substance. In those languages, obj.method(args) is described as "sending a message to obj," but mechanically it is a function call dispatched through a vtable. The message never exists as a thing. It is syntax.
| Kay's intent | What mainstream OOP delivered |
|---|---|
| A message is a thing: data that can be queued, logged, forwarded, inspected, or rejected | A message is a synchronous method call, which is a function call with dynamic dispatch |
| The receiver decides at runtime how to interpret a message (extreme late binding) | The sender binds at compile time to the receiver's type and method signature |
| Objects are autonomous, like separate machines; they could be asynchronous or remote | The caller blocks, and control flow is shared across objects |
| Encapsulation means state is reachable only through messages | Getters, setters, and shared mutable references leak state everywhere |
| The system is defined by the messages between parts | The system is defined by class hierarchies and inheritance taxonomies |
The language features grew around classes, because classes are what the compiler could see and check. Messages, the thing Kay cared about, were demoted to a calling convention. The idea survived elsewhere: partially in Smalltalk, more fully in Erlang's processes and mailboxes, in Hewitt's actor model, in Unix pipes, in Plan 9, and above all in the Internet, which Kay has pointed to as the one system that genuinely scaled his vision.
2. The Pi-Calculus: Messaging Made Mathematical
Robin Milner's pi-calculus is the formal theory of processes that communicate over named channels. Its core is small:
x̄⟨y⟩.P send the name y on channel x, then continue as P
x(z).Q receive on x, bind the result to z, then continue as Q
P | Q run P and Q in parallel
(νx)P create a fresh channel x, private to P
!P replicate P indefinitelyIts defining idea is mobility: channel names are themselves values that can be sent over channels. The communication topology is not fixed at design time. Processes hand each other the means to talk, and the network rewires itself as it runs.
Several properties of the pi-calculus line up closely with what Kay wanted and what dao.stream provides.
No addressee
In the pi-calculus you send to a channel, not to a process. Anyone holding the channel name can receive. This is already a step beyond Kay's objects, where a message still targets a specific receiver whose identity the sender must know. Milner acknowledged Hewitt's actors as an influence, and in that sense the pi-calculus is where the line from Smalltalk through actors became mathematics.
Mobility is topology
Passing a channel name in the pi-calculus corresponds to appending a stream descriptor as a value on another stream. The computation is not only what flows over the wires; it is also how the wires get rearranged. This is the same view that underlies treating macro expansion as stream topology in datom.world: an expander is not a feature of an evaluator but a process that sits on one side of a medium boundary and rewires what the other side sees.
Functions are processes, and continuations are channels
Milner showed that the lambda calculus can be encoded in the pi-calculus. A function call becomes: send the arguments along with a fresh return channel, then wait on that channel. The return channel is the continuation. Control flow turns into message flow.
yin.vm makes the same move at its stream boundary: a blocked read or an outbound call parks the continuation, and the stream it waits on is the return channel. Then it goes one step further. In the pi-calculus the continuation is only named by a channel. In yin.vm it is itself a value.
That holds for code and state alike. A program's Universal AST travels on a stream as tuples, each row addressed by the hash of its own content. The machine's CESK state, including continuations, is likewise plain data with no host objects in it, and the design projects that state into datoms. Once a continuation is a message on a medium, it can be persisted, inspected, and moved to another host, which is what makes migratory computation possible.
The asynchronous pi-calculus: the message becomes a thing
In the asynchronous pi-calculus (Honda and Tokoro, Boudol), an output x̄⟨y⟩ has no continuation. It is simply a term sitting in parallel with everything else until something consumes it. For the first time in this formal line, a message is a noun: a thing that exists independently of both its sender and its eventual receiver.
3. From Exchange to Publication
The pi-calculus formalizes messaging as exchange. dao.stream treats messaging as publication.
In the pi-calculus, receiving consumes: exactly one receiver gets each message, and once received it is gone. A message is a token that moves from one party to another. On dao.stream, reading is non-destructive. Each reader advances its own cursor, not the stream, so every reader sees every retained value, and nothing a reader does can take a value away from anyone else. A message is a fact that accumulates.
That is why stigmergy fits dao.stream and does not fit the pi-calculus. A pheromone trail is not consumed by the first ant that smells it. It stays in the environment, and every passing ant reads it and reacts in its own way.
4. What dao.stream Makes First Class
Putting the pieces together, dao.stream takes Kay's big idea and changes it in three ways.
The message is a value, not an event
A Smalltalk message exists only for the duration of its dispatch. A method call in Java exists only on the call stack. A value on a dao.stream outlives the act of writing it. It can be replayed, queried, and seen by observers who did not exist when it was written. How long it is retained is declared per stream, but no reader can destroy it by reading. Messaging stops being an act and becomes a medium.
Not even a known reader
Kay decoupled the receiver's implementation from the sender, and the pi-calculus, as section 2 showed, decoupled the receiver's identity. dao.stream completes the decoupling: the writer does not know who, if anyone, will read. This is Gelernter's generative communication from Linda, where messages exist independently of both parties, and it is the formal core of stigmergy.
dao.space is where the two halves meet, a hybrid of the pi-calculus and Linda's tuple space. Like the pi-calculus, agents write to a channel, their own dao.stream, and never to a process. Unlike the pi-calculus, nothing consumes it: each agent indexes its own stream and enqueues the index through a dao.jing intake pool, which stores it as immutable, content-addressed segments. Readers then find tuples by content with dao.space.query/match and dao.space.query/q, and that pairing of stream writes with associative reads is a Linda-style tuple space.
Interpretation belongs to the observer
In OOP, one receiver decides what a message means. On dao.stream, many observers read the same value and each interprets it in its own terms. yin.vm reads a tuple as code to run. dao.space reads it as an indexed fact. An expander reads it as syntax to rewrite. These observers are peers that remain ignorant of one another, connected only by the stream they share. This is late binding pushed to its limit: meaning lives in the interpreter, not in the structure. It is bound at read time, and it can differ for every reader.
5. The Inversion
In OOP, objects are primary and messages flow between them. You design the classes first and the messages fall out as method signatures.
In datom.world, the order is inverted. The stream is primary. What OOP would call objects (agents, virtual machines, indexes, expanders) are stable patterns of observation over the stream. They come and go. The facts remain.
Unix anticipated half of this inversion. A pipe carries no messages in Kay's sense, only an undelimited stream of bytes between two processes that know nothing about each other. The medium comes first, and the programs on either end are interchangeable. Plan 9 took the other half literally: every resource is a server, and every interaction with it is a 9P message. What neither kept is the history. A byte read from a pipe is gone, and a 9P request is answered and forgotten. dao.stream keeps the pipe's anonymity and adds retention.
Kay's lineage, read in this light, is a sequence of progressively more complete answers to one question: what is a message? The order below is conceptual, not chronological: Linda is older than the pi-calculus, and Java arrived after both.
- Mainstream OOP (C++ 1985, Java 1995): a function call with a receiver. A verb, and a synchronous one.
- Smalltalk and actors (1972, 1973): a dispatched request that the receiver interprets at runtime. Still a verb, but a late-bound one.
- The pi-calculus (1989 to 1992): a name sent over a mobile, anonymous channel. In the asynchronous pi-calculus, briefly a noun, until it is consumed.
- Linda (1985): a tuple in a shared space, matched by content. A noun, but consumed by
in. - Unix pipes (1973): a medium with no addressee and no messages, only bytes. Consumed as it is read.
- dao.stream: an immutable value in an append-only stream. A noun that reading never consumes, read by any number of observers, each supplying its own meaning.
That completes the conceptual argument. The rest of the post holds it to a stricter standard. If dao.stream really is the next step after the pi-calculus, it should be possible to say exactly which parts of the pi-calculus it keeps and which it gives up.
6. Is dao.stream a Pi-Calculus?
No, and it is not meant to be. dao.stream faithfully implements the channel half of the pi-calculus and deliberately omits the process half. The pi-calculus is a calculus of processes. dao.stream is a medium: its design contract says a stream carries values and decides nothing about them. Mapping the primitives one by one makes the boundary precise.
| Pi-calculus | dao.stream | Faithful? |
|---|---|---|
Channel x | Logical stream | Yes |
Fresh name (νx)P | create! | Mostly. The pi-calculus scopes names syntactically; dao.stream scopes them by possession of a descriptor, which acts as a capability |
Name mobility: sending x over y | A portable descriptor is plain data, so it can be append!ed as a value; a reader attach!es to it | Yes. This is the strongest correspondence |
Output x̄⟨y⟩ | append! | Matches the asynchronous pi-calculus: no continuation, never parks. One difference: a bounded transport may answer full as data, where a pi-calculus output always succeeds. Does not match the synchronous rendezvous of the original calculus |
Input x(z).Q | cursor and next | No. See below |
Parallel composition P | Q, replication !P, choice P + Q | Absent | Parallelism, replication, and choice belong to the interpreters and runtimes that read streams |
The communication rule x̄⟨y⟩.P | x(z).Q → P | Q{y/z} | Absent | dao.stream has no reduction semantics at all |
Where it breaks: input
Input in the pi-calculus does three things at once. dao.stream declines all three.
- Input consumes. As section 3 described, exactly one pi-calculus receiver gets each message, while every dao.stream cursor sees every retained value.
- Input blocks. A pi-calculus receiver waits until a message arrives. dao.stream's
nextis total and non-blocking. An empty position returns:dao.stream/blockedas data, the stream's equivalent ofEAGAIN, and the caller decides when to ask again. No operation parks. - Input binds a continuation. In the pi-calculus,
Qruns because the message arrived. dao.stream invokes nothing: it takes no callbacks, and interpreters decide when to callnext. The stream never causes computation.
The pi-calculus communication rule fuses synchronization, consumption, and substitution into one indivisible step. dao.stream pulls them apart and hands each one to whoever is reading.
The refusal to consume is a choice, not an oversight. The dao.stream contract excludes destructive reads by name: a read that removes what it observes turns one reader's progress into every other reader's data loss. That is coherent only while exactly one observer exists. Add a forwarder, an indexer, or a replica, and a destructive read becomes theft.
Taking is a write
The capability did not disappear. It moved, and it split in two. Consuming receive in the pi-calculus does two jobs at once, and datom.world gives each job its own home.
- Removal (the message no longer holds) becomes a retraction datom in dao.space. Linda's
inbecomes observe-then-retract: an append, not a stream operation. - Exclusion (only one receiver wins) becomes a lease, today a proposed design rather than shipped code: a grant, issued by a grantor on its own stream, that lapses unless the holder renews it. Leases are ordinary facts on ordinary streams and add nothing to the dao.stream contract.
The hard part of a take was never the removal. It was excluding competing takers, and that is something no stream can promise. A naive claim pattern shows why. If each worker appends a claim datom to its own single-writer stream and queries for unclaimed work, two workers can both run the query before either claim becomes visible, and both take the same task. Neither stream knows the other exists, so no layer can prevent the race or even detect it. Resolving it is interpreter policy: a reader-side tie-break with a genuinely shared clock, a single shared claims stream, a lease, or simply accepting both claims.
The pi-calculus communication rule is atomic across every possible receiver, which silently assumes a global arbiter. The lease design makes the arbiter explicit and local: a lease grantor whose single-writer stream orders its grants. Consuming receive is then a composition:
consuming receive = cursor read + lease on the message + retraction datomThe cost is visible rather than hidden: a round trip to a grantor, and a timeout for holders that go silent. And nothing is lost for anyone else. The original message remains in the stream for every observer that was not competing for it: indexers, auditors, replicas. In the pi-calculus it would simply be gone.
Other mismatches, in both directions
Any number of processes may send on a pi-calculus channel, while datom.world streams are typically single-writer, so many-to-one communication becomes many streams merged by the reader. And the pi-calculus receives by channel name only; content-based matching in the Linda style lives one layer up, in dao.space.
In the other direction, dao.stream has properties the pi-calculus has no term for: replay from the oldest retained value or from any kept cursor, gap outcomes when history has been evicted, a close! that ends future availability without erasing the past, and every operational outcome returned as data rather than as behavior.
dao.stream can be modeled inside the pi-calculus, as a replicated server process that holds history and answers cursor requests. That works, but the property that matters most, a shared history that no one consumes, becomes an encoding rather than a primitive.
The short version: dao.stream is the pi-calculus's topology without its reductions. Names, mobility, and asynchronous output survive. Consuming input, synchronization, and process terms do not. The reduction semantics that the pi-calculus builds into communication are exactly what datom.world keeps in its interpreters.
7. Is dao.stream Plus yin.vm a Pi-Calculus?
If the reductions live in the interpreters, the natural follow-up is whether dao.stream together with an interpreter like yin.vm adds up to the pi-calculus. It comes much closer, but the answer is still no. Here is what yin.vm restores.
| Pi-calculus feature | dao.stream plus yin.vm |
|---|---|
Fresh name (νx)P | :stream/make (yes) |
| Name mobility | Streams are values in the language (yes) |
Output x̄⟨y⟩ | :stream/put, which has no continuation to synchronize with, as in the asynchronous pi-calculus. It parks only when a bounded transport answers full (yes) |
Blocking input bound to a continuation x(z).Q | :stream/next yields blocked, the continuation parks, and it is woken when data arrives. The parked continuation plays the role of Q. Park and resume are part of the machine relation, not a host feature (yes) |
Replication !P | A recursive server loop (yes, by encoding) |
Parallel composition P | Q | Partial. The machine schedules many parked and woken continuations, but the language has no spawn primitive yet; parallelism mostly comes from many VMs and agents side by side |
| Consuming receive | Not as a primitive. Each VM owns its cursor, so two VMs reading the same stream both receive the message. Composable from a lease and a retraction (section 6) |
Guarded choice x(z).P + y(w).Q | No. There is no primitive that waits on several streams and commits to whichever fires first |
Broadcast is the real divide
Exactly-one delivery is what lets the pi-calculus express mutual exclusion, locks, and resource handoff directly. Delivering every message to every reader is broadcast semantics, where exclusion would come back only through an explicit lease, and the closest formal relatives are the broadcast calculi, the broadcast pi-calculus and Prasad's Calculus of Broadcasting Systems (CBS) in asynchronous form, rather than the pi-calculus itself. Even they differ in one respect: a broadcast is heard only by whoever is listening at that moment, while a dao.stream retains its values, so a reader that arrives late still sees them. In that respect it is closer to Linda's non-destructive rd over a persistent log.
Nondeterminism becomes a fact
In the pi-calculus, which receiver wins and which branch of a choice fires are nondeterministic and unrecorded. yin.vm's design direction is execution that is deterministic over its datom log: scheduling decisions such as park events would themselves be logged, and cursor positions derived from them, so a run could be replayed exactly. This is not built yet. Today a park is visible only as an optional, lossy telemetry snapshot. The pi-calculus treats an execution as a space of possible reductions. The yin.vm design treats it as a history. That is a conceptual addition, not just an implementation detail.
What "faithful" would require
Showing that a system faithfully implements the pi-calculus has a precise meaning: an encoding that is fully abstract, so that two pi-calculus terms are behaviorally equivalent (bisimilar) exactly when their encodings are. No such encoding has been given for dao.stream plus yin.vm, and given broadcast receive, one would not hold for the standard pi-calculus.
Closing the gap would take three additions: guarded choice over several streams, a spawn primitive that adds a process to the run queue, and consuming receive. Choice and spawn fit naturally into the existing park and wait-set machinery. Consuming receive cannot become a stream operation without breaking the architecture, but it can be composed from a lease and a retraction. It is the one piece that has to be built from the outside rather than added to the machine.
The gap also points to a practical resource. Questions like "which observers can see which streams," "how do stream references propagate," and "are these two wirings equivalent" are exactly the questions process calculi were built to answer. The broadcast calculi above, together with the join calculus for pattern-based reception, are the natural starting point for a formal semantics of dao.stream topology. Much of that theory already exists.
So dao.stream plus yin.vm is a process calculus with mobile channels, asynchronous output, blocking input, and broadcast receive, with recorded execution as its design direction. It is a close cousin of the pi-calculus that keeps the reductions stigmergy needs and replaces "one receiver consumes the message" with "every observer reads the fact."
Conclusion
The most important property of object-oriented programming was the one its most popular languages implemented worst. Messaging was reduced to method dispatch, while classes and inheritance took center stage. The pi-calculus recovered messaging and gave it mathematical precision, but it modeled messages as tokens to be exchanged and consumed.
dao.stream keeps what each step got right: Kay's autonomous entities, the pi-calculus's anonymous mobile channels, and Linda's generative communication. Then it adds the property all of them lacked: reading never destroys the message. Consumption, where it is needed, becomes an explicit write rather than a side effect of observation. A message becomes a fact. Facts accumulate. Observers come and go, each reading the same history in its own way.
OOP made the message a verb. The pi-calculus and Linda made it a noun that someone consumes. dao.stream makes it a fact.