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 intentWhat mainstream OOP delivered
A message is a thing: data that can be queued, logged, forwarded, inspected, or rejectedA 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 remoteThe caller blocks, and control flow is shared across objects
Encapsulation means state is reachable only through messagesGetters, setters, and shared mutable references leak state everywhere
The system is defined by the messages between partsThe 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 indefinitely

Its 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-calculusdao.streamFaithful?
Channel xLogical streamYes
Fresh name (νx)Pcreate!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 yA portable descriptor is plain data, so it can be append!ed as a value; a reader attach!es to itYes. 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).Qcursor and nextNo. See below
Parallel composition P | Q, replication !P, choice P + QAbsentParallelism, 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}Absentdao.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 next is total and non-blocking. An empty position returns :dao.stream/blocked as data, the stream's equivalent of EAGAIN, and the caller decides when to ask again. No operation parks.
  • Input binds a continuation. In the pi-calculus, Q runs because the message arrived. dao.stream invokes nothing: it takes no callbacks, and interpreters decide when to call next. 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 in becomes 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 datom

The 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 featuredao.stream plus yin.vm
Fresh name (νx)P:stream/make (yes)
Name mobilityStreams 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 !PA recursive server loop (yes, by encoding)
Parallel composition P | QPartial. 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 receiveNot 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).QNo. 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.