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.”