Why Simula — Not Smalltalk — Changed the World: The Power of Simulation and Coroutines

When we trace the genealogy of modern programming, we often celebrate Smalltalk as the progenitor of object-oriented programming. Yet this narrative obscures a deeper truth: Simula, created by Ole-Johan Dahl and Kristen Nygaard in 1962–1967, fundamentally transformed how we model complex systems, and its influence extends far beyond the syntax of classes and objects that Smalltalk popularized.

Smalltalk gave us elegant syntax, live programming environments, and a pure vision of message passing. But Simula gave us something more profound: the ability to directly encode the structure of reality itself into executable models. Where Smalltalk was a language for building systems, Simula was a language for understanding systems.

The Conceptual Revolution

Simula emerged not from computer science departments, but from the Operations Research section of the Norwegian Computing Center, where Dahl and Nygaard were wrestling with simulation problems for industrial processes. They needed to model ships moving through fjords, production lines with queues, and telecommunications networks with competing demands.

The breakthrough wasn’t just “bundling data with procedures” — that’s the reductionist view that misses the point. The breakthrough was recognizing that complex systems consist of semi-autonomous entities that maintain their own state, interact through well-defined protocols, and evolve over time. This insight predates and transcends the later “everything is an object” philosophy of Smalltalk.

Consider the fundamental difference in motivation:

Smalltalk: How can we build a computational environment where everything is uniform, reflexive, and malleable?

Simula: How can we express the inherent structure of real-world systems — manufacturing plants, traffic networks, biological processes — in a way that lets us reason about their behavior?

One is inward-looking (computational aesthetics), the other outward-looking (epistemic fidelity to reality).

Coroutines: Encoding Time Itself

Simula’s most profound innovation wasn’t inheritance or encapsulation — it was coroutines with quasi-parallel execution. This feature, largely absent from Smalltalk, gave programmers the ability to directly encode temporal relationships and causal chains.

The Coroutine Model

In Simula, every object could be a coroutine — an independent locus of execution with its own instruction pointer and local state. Three primitives enabled this:

detach - Suspend execution and return control to the activator resume - Transfer control to another object, continuing where it left off
call - Like resume, but create a return point for when the called object detaches

Here’s a canonical example — a producer-consumer system:

! Simula Producer-Consumer with Coroutines

begin
class Buffer;
begin
integer array items(1:10);
integer count, writePos, readPos;
procedure put(item); integer item;
begin
items(writePos) := item;
writePos := if writePos = 10 then 1 else writePos + 1;
count := count + 1;
end;
integer procedure get;
begin
integer item;
item := items(readPos);
readPos := if readPos = 10 then 1 else readPos + 1;
count := count - 1;
get := item;
end;
count := 0;
writePos := 1;
readPos := 1;
end Buffer;

ref(Buffer) shared;
class Producer(id, total); integer id, total;
begin integer i; for i := 1 step 1
until total do
begin
while shared.count = 10 do
detach; ! Buffer full, yield control
shared.put(id * 100 + i);
outtext("Producer "); outint(id, 2);
outtext(" produced: "); outint(id * 100 + i, 5);
outimage;
detach; ! Voluntarily yield after each item
end;
end Producer;
class Consumer(id, total); integer id, total;
begin integer i, item;
for i := 1 step 1 until total do
begin while shared.count = 0 do
detach; ! Buffer empty, yield control
item := shared.get;
outtext("Consumer "); outint(id, 2);
outtext(" consumed: "); outint(item, 5);
outimage;
detach; ! Voluntarily yield after each item
end; end Consumer;
ref(Producer) prod1, prod2;
ref(Consumer) cons1;
shared :- new Buffer;
prod1 :- new Producer(1, 5);
prod2 :- new Producer(2, 5);
cons1 :- new Consumer(1, 10);
! Coroutine scheduling - explicit, deterministic
while prod1.count < 5 or
prod2.count < 5 or cons1.count < 10 do
begin prod1.resume; ! Run until detach
prod2.resume; ! Run until detach
cons1.resume; ! Run until detach
end;
end

Notice what’s happening here: Each object maintains its own execution context. When prod1.resume is called, execution continues exactly where that producer left off—mid-loop, with all local variables intact. When it hits detach, control returns to the scheduler.

This isn’t threads with locks. This isn’t async/await. This is direct encoding of cooperative temporal behavior — objects that naturally interleave their actions in a deterministic, reproducible way.

Why This Matters: Discrete Event Simulation

Simula’s creators embedded these coroutine primitives into a Simulation class that provided discrete event simulation capabilities:

! Bank Teller Simulation in Simula

Simulation begin class Customer; begin real arrivalTime, serviceStart, serviceEnd; arrivalTime := time; ! Current simulation time ! Request service from teller if teller.idle then begin serviceStart := time; hold(normal(5.0, 1.0, randomStream)); ! Service time serviceEnd := time; outtext("Customer served: "); outfix(serviceEnd - arrivalTime, 2, 6); outtext(" total time"); outimage; end else begin ! Join queue into(tellerQueue); passivate; ! Suspend until reactivated serviceStart := time; hold(normal(5.0, 1.0, randomStream)); serviceEnd := time; outtext("Customer waited: "); outfix(serviceStart - arrivalTime, 2, 6); outtext(", served in: "); outfix(serviceEnd - serviceStart, 2, 6); outimage; end; end Customer; class Teller; begin boolean idle; idle := true; while true do begin if not tellerQueue.empty then begin ref(Customer) next; idle := false; next :- tellerQueue.first; out(next); activate next; ! Resume waiting customer end else idle := true; hold(0.1); ! Check queue periodically end; end Teller; class CustomerGenerator; begin integer i; for i := 1 step 1 until 20 do begin activate new Customer; hold(exponential(3.0, randomStream)); ! Inter-arrival time end; end CustomerGenerator; ref(Teller) teller; ref(Head) tellerQueue; ref(RandomStream) randomStream; randomStream :- new RandomStream(12345); tellerQueue :- new Head; teller :- new Teller; activate new CustomerGenerator; hold(100.0); ! Run simulation for 100 time units end Simulation;

Look at what Simula provides here:

time - Global simulation clock
hold(duration) - Suspend this object and schedule its resumption after duration
activate - Schedule another object to run
passivate - Suspend indefinitely until explicitly reactivated
Event queue - Automatically managed, sorted by scheduled time

When you write hold(5.0), you're not just sleeping a thread. You're expressing a causal relationship: "This customer's service takes 5 time units, and nothing about this customer matters until that duration elapses."

The simulation framework maintains an event queue, advances the global clock, and deterministically processes events in temporal order. Every run with the same random seed produces identical results — critical for analyzing complex systems.

Why Simula Changed the World (and Smalltalk Didn’t, Quite)

1. Simula Enabled Domain Modeling, Not Just Software Architecture

Smalltalk gave us MVC, design patterns, and agile development. These are valuable, but they’re software engineering concerns.

Simula gave us the ability to model:

Manufacturing systems (the original motivation)

Telecommunications networks (Ericsson used it extensively)

Biological ecosystems

Economic systems

Social dynamics

When Bjarne Stroustrup created C++, he wasn’t inspired by Smalltalk’s message passing or live environment. He was inspired by Simula’s ability to directly represent domain concepts as classes. When the Gang of Four wrote Design Patterns, many patterns (State, Strategy, Command) are essentially formalizations of techniques Simula programmers had used for decades to model real-world systems.

2. Coroutines Anticipated Modern Concurrency Models

Simula’s coroutines directly influenced:

CLU’s iterators (Barbara Liskov, 1970s)

Modula’s coroutines (Niklaus Wirth)

Python’s generators (PEP 255, inspired by CLU)

JavaScript’s async/await (cooperative, single-threaded)

Go’s goroutines (though these can be parallel)

Every modern language with yield, generators, or cooperative multitasking owes a debt to Simula's detach and resume.

Smalltalk, meanwhile, had Process objects and semaphores—useful for concurrency within the environment, but not conceptually exportable in the same way. When you use Python's yield today, you're using Simula's ideas, not Smalltalk's.

3. Simula’s Influence Is Structural, Not Stylistic

Languages influenced by Smalltalk: Objective-C, Ruby, arguably Scala
Languages influenced by Simula: C++, Java, C#, Eiffel, Python, Beta — essentially every mainstream OO language

Why? Because Simula’s model — static typing, classes as types, inheritance hierarchies, virtual methods — maps more naturally to how hardware works and how large teams coordinate.

Smalltalk’s model — dynamic typing, metaclasses, message passing, live environments — is more powerful in many ways, but requires a paradigm shift that industry largely rejected in favor of Simula’s more conservative approach.

The Irony of History

Here’s the irony: Simula solved harder problems than Smalltalk, but Smalltalk got more academic attention because it was philosophically purer and more radical.

Simula said: “Let’s extend Algol with classes, coroutines, and simulation facilities — tools for modeling complex systems.”

Smalltalk said: “Let’s build a minimal, reflexive computational universe where everything is an object and everything is a message.”

Academia loves the second story. It’s clean, revolutionary, inspiring. But industry needed the first story. They needed to model air traffic control systems, telecommunications networks, factory floors. They needed engineering tools, not computational philosophy.

When you look at modern software — distributed systems with microservices (objects), event-driven architectures (coroutines/async), domain-driven design (Simula’s class modeling), agent-based simulations (quasi-parallel entities) — you’re seeing Simula’s vision fully realized.

Smalltalk gave us important ideas about programming environments and reflective systems. But Simula gave us the conceptual vocabulary for how we think about complex systems themselves.

Conclusion

Simula changed the world not because it had cleaner syntax than Smalltalk, but because it encoded deeper truths about how complex systems work. It recognized that the world consists of semi-autonomous entities with internal state, engaged in quasi-parallel interactions, unfolding over time.

Smalltalk showed us that computation could be uniform and reflexive. But Simula showed us that computation could mirror reality — and in doing so, help us understand and predict it.

When Dahl and Nygaard received the Turing Award in 2001, the citation read: “for ideas fundamental to the emergence of object-oriented programming.” But perhaps it should have read: “for showing us how to encode time, causality, and structured complexity into executable models.”