DEV Community

Fred Ross
Fred Ross

Posted on • Edited on

Object oriented programming, the beginning

When you learn to program today, you encounter something called "object oriented programming." Everyone argues over what is or is not object oriented. Alan Kay invented the term, and people even argue with him.

There's a relevant line from Tom deMarco's The Deadline: A novel about project management: "When I come across anything unclear in a specification I go nosing around for conflict. I always find it." If there is this much argument for this long, that means that there are really different practices all trying to share the name object oriented programming. None of them are wrong. They're all shaped by their particular needs and conditions, but to an outsider or someone who has only lived in one of the traditions the whole morass is bewildering.

Let's take a tour through these various traditions. I'll start with what all of them acknowledge as the beginning: the language Simula-67.1

Simula took ALGOL 60, the lingua franca of imperative languages at the time, and adapted it for simulating discrete systems. As a very simple example, consider three checkout stations at the grocery store. People arrive at some rate to check out and go to the checkout station with the shortest line. The checkout stations take some random amount of time to handle each customer, at which point the customer disappears from the simulation. Our entities are the three checkout stations and the customers.

In ALGOL 60, we would write a program like (I'm using Python since it has all the features we'll need and is much more familiar and available than ALGOL 60 these days):

import random
import queue


def argmin(xs):
    """Return the index of the minimum value in the list xs.

    If the minimum value appears multiple times, return the index
    of its first appearance. If the list is empty, return None.
    """
    if len(xs) == 0:
        return None
    elif len(xs) == 1:
        return 0
    else:
        idx = 0
        val = xs[0]
        for i, v in enumerate(xs):
            if v < val:
                idx = i
                val = v
        return idx


def peek_priority(q):
    """Return the priority of the first element in the priority queue q.

    This does not change the contents of q, so q.get() will get the
    same element whether we run this function first or not.
    """
    return q.queue[0][0]


customer_arrival_rate = 1.0  # customer/minute
shortest_checkout_time = 1.0  # minutes
longest_checkout_time = 5.0  # minutes

# We will enqueue events with the time they
# are supposed to happen as their priority.
# We will enqueue (time, checkout line) for
# events of a customer finishing at the specified
# checkout line, and (time, -1) for a new customer
# arriving to join a line.
event_queue = queue.PriorityQueue()

CUSTOMER_ARRIVES = -1

n_checkout_lines = 3
checkout_line_lengths = [0] * n_checkout_lines
next_customer_arrives_at = random.expovariate(customer_arrival_rate)

# We run the simulation for 100 events. We have to
# stop it somewhere or it runs forever.
total_customers = 30
customers = 0
while True:
    if not event_queue.empty():
        # Customers might arrived in the time before
        # the next event in the queue. We peek that time and then
        # add customer arrival events to the queue until
        # we pass it.
        timestamp = peek_priority(event_queue)
        while timestamp > next_customer_arrives_at and customers < total_customers:
            event_queue.put((next_customer_arrives_at, CUSTOMER_ARRIVES))
            customers += 1
            next_customer_arrives_at += random.expovariate(customer_arrival_rate)

        timestamp, event = event_queue.get()
        if event == CUSTOMER_ARRIVES:
            shortest_line = argmin(checkout_line_lengths)
            checkout_line_lengths[shortest_line] += 1
            checkout_time = random.uniform(
                shortest_checkout_time, longest_checkout_time
            )
            event_queue.put((timestamp + checkout_time, shortest_line))
            print(
                f'{timestamp:6.2f} - ({" ".join(str(v) for v in checkout_line_lengths)}) '
                f"- Customer joined line {shortest_line} "
            )
        else:
            checkout_line_lengths[event] -= 1
            print(
                f'{timestamp:6.2f} - ({" ".join(str(v) for v in checkout_line_lengths)}) '
                f"- Line {event} finished with customer "
            )

    elif customers < total_customers:  # queue is empty, enqueue a customer
        event_queue.put((next_customer_arrives_at, CUSTOMER_ARRIVES))
        customers += 1
        next_customer_arrives_at += random.expovariate(customer_arrival_rate)
    else:
        break

Simula was designed so that we could write each entity's behavior separately instead of interleaving everything as we have here.

Its idea of simulation was to have distinct entities emitting and awaiting discrete events. You wanted to be able to send the same events to different entities in the simulation and have them handle them differently, so it introduced the idea of classes of entities and subclasses. Subclasses could have their own behavior for responding to given events.

The event handling itself was done by letting you define coroutines on the classes. We'll emulate this with Python's asyncio, which is awkward since it wasn't meant to be a simulation system, but lets us see each checkout station and the source of customers written as their own scripts and allowed to interact as we would have in Simula-67.

import random
import asyncio
import time
import collections
import math

# This simulation runs in real time as opposed to
# as fast as the event loop can spin. For the sake
# of not waiting forever, we speed up the rates by
# scaling all rates uniformly.
scale = 100.0

customer_arrival_rate = 1.0 * scale  # customer/minute
shortest_checkout_time = 1.0 / scale  # minutes
longest_checkout_time = 5.0 / scale  # minutes


def argmin(xs):
    """Return the index of the minimum value in the list xs.

    If the minimum value appears multiple times, return the index
    of its first appearance. If the list is empty, return None.
    """
    if len(xs) == 0:
        return None
    elif len(xs) == 1:
        return 0
    else:
        idx = 0
        val = xs[0]
        for i, v in enumerate(xs):
            if v < val:
                idx = i
                val = v
        return idx


# Our first entity is the checkout station. It maintains a list
# of how long each customer queued will take to check out.
class CheckoutStation:
    def __init__(self, i):
        self.line_length = 0
        self.i = i  # For reporting, we want to know which checkout station this is
        self.closed = False  # Bookkeeping so we can shut down gracefully

    def queue_length(self):
        return self.line_length

    def join_queue(self):
        self.line_length += 1
        return self.line_length

    def close_station(self):
        self.closed = True

    async def run(self):
        while True:
            if self.line_length > 0:
                checkout_time = random.uniform(
                    shortest_checkout_time, longest_checkout_time
                )
                await asyncio.sleep(checkout_time)
                self.line_length -= 1
                report(
                    f"Queue {self.i} finished with customer in {checkout_time*scale:.2f} seconds."
                )
            elif self.line_length == 0 and self.closed:
                report(f"Closed queue {self.i}.")
                return
            else:
                await asyncio.sleep(0)


# And here is the entity that generates customers and has them
# queue.
async def customer_source(stations):
    for i in range(30):
        time_until_customer = random.expovariate(customer_arrival_rate)
        await asyncio.sleep(time_until_customer)

        queue_lengths = [st.queue_length() for st in stations]
        shortest_queue = argmin(queue_lengths)
        n = stations[shortest_queue].join_queue()
        report(f"Customer joined queue {shortest_queue}, position {n} in line")
    report("All customers arrived.")
    for st in stations:
        st.close_station()


n_stations = 3
stations = [CheckoutStation(i) for i in range(n_stations)]

start_time = time.time()


def report(msg):
    queue_lengths = [st.queue_length() for st in stations]
    print(
        f'{(time.time() - start_time)*scale:6.2f} - {" ".join(str(v) for v in queue_lengths)} '
        f"- {msg}"
    )


loop = asyncio.get_event_loop()
futures = [st.run() for st in stations] + [customer_source(stations)]
loop.run_until_complete(asyncio.gather(*futures))

The key passages are (amended slightly to take out the edge cases from making the simulation terminate and the reporting code):

while True:
    time_until_customer = random.expovariate(customer_arrival_rate)
    await asyncio.sleep(time_until_customer)

    queue_lengths = [st.queue_length() for st in stations]
    shortest_queue = argmin(queue_lengths)
    n = stations[shortest_queue].join_queue()

and

while True:
    if self.line_length > 0:
        checkout_time = random.uniform(
            shortest_checkout_time, longest_checkout_time
        )
        await asyncio.sleep(checkout_time)
        self.line_length -= 1
    else:
        await asyncio.sleep(0)

The behavior of each entity is separate and clear. Compare that to the ALGOL 60-esque version. For this simple simulation, we were able to write it either way, but think about simulations involving thousands of entities interacting in many ways. With the Simula approach, it's a lot of work, but it's straightforward. With the ALGOL 60 approach, it would be a monumental struggle.

This idea of objects modelling entities and defining hierarchies of classes and subclasses of entities persists in some traditions of object oriented programming. In others it has been rejected in favor of very different approaches.

The notions of class and method familiar from modern languages like Java, Python, or Ruby are all in place in Simula-67. Many people stop here.


  1. That 67 stands for the version of Simula released in 1967. Naming your language version with the year has been pretty typical for most of the history of computing, e.g., ALGOL 60, FORTRAN 77, C 99, and C++ 14. 

Top comments (2)

Collapse
 
aminmansuri profile image
hidden_dude

While Simula was the first to have some of these features. It really was Alan Kay's team's invention Smalltalk that shaped what today is considered OO.

Smalltalk was simple (simpler than Python) its syntax can be explained in one line:

      object message.

You have some instance of an object and you send it a "message" that message "selects" a method in the inheritance hierarchy and executes it. You can also do things like:

     object message: otherObject andAlso: anotherObject.

This would pass otherObject and anotherObject to the method selected by message:andAlso: in object's class hierarchy.

That's pretty much Smalltalk's entire syntax. To create classes, define methods, etc.. you use this syntax.
(there is some additional punctuation for local variables, semi-colons, etc.. but its minimal)

"Modern" OO languages suffer from excessive bloat. Mainly because:

  1. Smalltalk was ahead of its time
  2. Smalltalk relied on a VM and garbage collection in the era of 64K RAM. (Ie. PCs in 1980 couldn't handle its size/RAM requirements)
  3. Other barriers such as the fact that it was interpreted, etc..

All those barriers no longer exist today, so people have slowly introduced Smalltalk features in Python, then Java, then Ruby. But none of these approach the Smalltalk simplicity. Its too bad it didn't catch on.

Collapse
 
madhadron profile image
Fred Ross

I know Smalltalk, and I think object oriented programming as taught and practiced broadly today owes more to Simula. The idea that the language itself should be a programmable thing was the key of Smalltalk. Python, Ruby, etc. are all still using it as a way to create fancy record types.

And I originally planned to make this a series of posts, going into Smalltalk, then exploring the later lineages (Grady Booth's super structured approach, Self's getting rid of classes), but realize that I didn't have time.