Notes on Project Oberon: thoughts about abstraction

I recently read the first part of Project Oberon’s book. I thought that was everything to read about it until I wrote this post, and then saw there are a couple more parts, which are now on my reading list, so this post will be only about the first part linked above.

Two things caught my attention in that document, and I want to share my thoughts about them here.

The first one is about abstractions. The document has this to say about them:

Abstraction is indeed the key of any modularization, and without modularization every hope of being able to guarantee reliability and correctness vanishes.

On a first read, this seems logical. I’d guess that for most people writing software this seems logical too. However, I’ve been doing some thinking on abstractions for a while, and after reading this I realised that abstractions actually prevent us from guaranteeing reliability and correctness at the level of the whole system.

The key property of an abstraction is to hide certain “implementation details” so people can work with them using a standard interface (that of the abstraction).

The document also agrees with this:

Every abstraction inherently hides details, namely those from which it abstracts.

This means that as long as we view a system only through its abstractions, we can indeed guarantee some degree of reliability and correctness.

However, we can’t say the same about the whole system. It is built with components that may also act in different ways that aren’t covered by the abstraction (I’d guess that almost all components will behave like this). This means that if you actually want to guarantee reliability and correctess of the entire system, you have to pierce the “abstraction veil” and look at the actual components being used in it. You must understand all of their properties and behaviours, not just the ones that are exposed through abstractions.

For example, you can’t study a system’s reliability without understanding its performance properties, and you can’t study performance properties only looking at abstractions, because most of them do not include performance properties at all. To build an understanding of performance properties you have to look at the actual thing behind the abstraction.

It seems that we don’t want abstractions when trying to study certain things about a whole system. Instead, we want to view all of its components and then build our understanding from there. Abstractions hide the things that we care about.

The document adds “modularization” as a slight level of indirection. I read “modularization” with the meaning of grouping some things together and only providing an abstract interface for other modules to use. In this case, all that I said above applies.

But it’s also possible to modularise something without hiding any implementation details from other mdoules that want to connect to it. In this case, modules are purely a mechanism to logically group some components and make it easier for other humans to work with them, or talk about them. I think it’s somewhat clear that with this narrower meaning, we can group a system in modules, but still have a full view of it that allows us to study its reliability and correctness at the whole system level, because there’s no abstracting happening.


As a system evolves, it’s likely that some abstractions will have to change. Imagine some hardware that introduces a feature specific to that hardware, which is not covered by any existing abstraction. Users relying on the abstraction won’t be able to take full advantage of their hardware.

To correct this, I usually see two approaches that are used:

  1. Change the abstraction by adding something to it to allow the new feature to be used.
  2. Bypass any abstractions and get users to directly use the feature.

With 1, the abstraction tends to become “sparse”, in the sense that the new thing added to the abstraction will be done only to allow one specific feature to be used. In this case, there is no abstraction of multiple implementations, but just an extra step that people have to go through to use the feature.

In my opinion, this is a sign of “premature abstraction” (to mirror the idea of premature optimisation). Taken to the extreme, this approach ends in a “bag of APIs” scenario, in which case the purpose of an abstraction gets weakened, because there is no cohesive interface through which features can be used.

With 2, the purpose of the abstraction weakens as well, because users will have to ignore it for certain cases. The more features that get added without a change in the abstraction, the weaker the abstraction gets.

In both cases, the tendency is to weaken the abstraction, and if this is allowed to continue, it’s likely the abstraction will stop being used completely.

Both of these cases assume that there is only “one” abstraction through which users should use the system (or at least a specific part of the system).

Oberon’s approach is to define other/more abstractions. Any module can add abstractions in the system, and users can then make use of these new abstractions. Since the existing abstractions tend to get weaker as new features are built, then why not create new abstractions instead?

The important understanding is that existing abstractions shouldn’t be “extended” with new functionality, even if the change is backwards compatible. Fixing bugs is fine, but adding new functionality to the same abstraction isn’t. Just create a new one.

This allows the system to evolve with little effort, and allows abstractions to grow naturally.

When a single feature is added, there isn’t a need to introduce abstractions. A new module can allow direct access to the feature. As other components start implementing the feature (e.g. more hardware supporting some feature), a common interface will naturally emerge, and then a new abstraction can be created to provide the new feature through that interface. No need to touch existing abstractions at all.

It seems that a consequence of this approach is that it’ll create more maintenance overhead - changes will have to take into account multiple abstractions that they can affect. I’ll leave more discussion about this for another post, but I think this is solved by the same philosophy of adding new things and leaving existing ones untouched. In cases where existing things have to be changed, I think better tooling would alleviate almost all problems. Creating good tooling is something our field is surprisingly bad at, though.

It also seems that application code would have to change to use newer abstractions instead of the older ones, but that isn’t the case: existing abstractions can still be used normally, because they are supported by the system. Just because a new abstraction is added, it doesn’t mean the “old” one ceases to function - that’s what makes the Oberon approach so interesting.


The Project Oberon document also mentions a convention that it uses in the language used to build software for it:

Module names in the plural form typically indicate the definition of an abstract data type in the module.

It was only after reading this that it occurred to me that we’ve been overloading names with functionality for a long time. We have a lot of ways to change names in our code to add extra meaning to them. The hungarian notation is a somewhat popular example of this. Other examples include prepending interfaces with “I”, dictating the order of verbs/nouns/adjectives when creating names, changing the case depending on the modifiers/meaning of a name (UPPER_CASE for constants, camelCase for functions).

The more I think about this, the more it seems that this comes from us using plaintext for code. I’m writing a (much longer) post about this subject, so I won’t dive into it here, but it’s an interesting observation that might be worth thinking about for a while.