Photo by Alessia Cocconi on Unsplash

The Magic of Abstractions

Dominik Tornow

--

In arguably the most fundamental work in computer science, Structure and Interpretation of Computer Programs by Abelson and Sussman, the term abstraction appears on page one, chapter one, headline one.

Abelson and Sussman identify Abstractions as one of the 3 fundamental building blocks of computer programs: Primitive expressions, means of combinations, and means of abstractions.

  1. Primitive expressions which represent the simplest entities of the language.
  2. Means of combination by which compound elements are built from simpler elements.
  3. Means of abstraction by which compound elements are named and manipulated as a unit.

While this definition accurately captures the nature of abstractions, it does not stress their essence: Abstractions have the power to transform a domain of discourse, effectively, abstractions reshape entire worlds.

Turning the ugly into the beautiful

In the book Modern Operating Systems, Tanenbaum and Bos use Figure 1. to illustrate the transformative nature of abstractions.

The lower-level presents a set of entities, the ugly entities, that the higher-level consumes and transforms into a different set of entities, the beautiful entities.

From Hardware to Operating System

For instance, from the perspective of an operating system, the hardware components such as the CPU, RAM, and disks represent the ugly interface. The operating system takes in these entities and presents a beautiful interface, processes, memory, and files.

From Operating System to Database System

However, beauty lies in the eye of the beholder. For example, from the point of view of database systems, the operating system presents its ugly interface, now processes, memory, and files are undesirable. Now the database system consumes these entities and presents its beautiful interface, tables consisting of rows and columns, and transactions.

Turtles all the way down

This example of databases and operating systems also emphasizes the recursive relationships that exist between abstractions. Higher-level abstractions are built upon lower-level abstractions until we reach the fundamental primitives, which represent the simplest entities in the system.

Each time we transition across these boundaries, moving from a database system to an operating system and from an operating system to hardware, we enter an entirely distinct world. This entails encountering different entities, relationships among those entities, and constraints on those relationships. Each transition presents an entirely different semantic.

Example • Transactions

Transactions stand as one of the most ubiquitous abstractions ever created. Almost every software engineer knows of transactions and their principles encapsulated by ACID, the acronym for Atomicity, Consistency, Isolation, and Durability.

Transactions are the benchmark for an exceptional developer experience:

Transactions allow you to pretend that concurrency or failure do not even exist!

We will explore the concept of transactions, focusing on the atomicity of transactions, to illustrate the magic of abstractions.

Overview

A transaction is a sequence of operations on a set of objects that ends with exactly one commit or exactly one abort. A transaction has all-or-nothing semantics, that is, when a transaction commits, all operations take effect and when a transaction aborts, no operation takes effect.

A common implementation strategy is via an Undo Log: Before executing an operation on an object, the system records the corresponding undo operation in the Undo Log.

On abort, the operations in the Undo Log are applied, effectively rolling back changes up to this point.

Transactions on an Application Level

We can implement this strategy on an application level, therefore, from the point of view of the application developer, explicitly handling the recording of undo operations.

# the data store
store = {}
# the undo log
undos = []

# Example usage
try:
# set key1
undos.append(('SET', 'key1', store.get('key1')))
store['key1'] = 'value1'
# set key2
undos.append(('SET', 'key2', store.get('key2')))
store['key2'] = 'value2'
# some error happens
raise Exception('Some error')
except Exception as e:
while undos:
o, k, v = undos.pop()
if o == 'SET':
if v is None:
del store[k]
else:
store[k] = v

Here, before we modify the data store, we record an undo operation. Then, if an exception happens, we apply the undo operations in reverse order in the exception handler.

The example usage is failure-aware, you have to think about the possibility of failure to guarantee all-or-nothing semantics.

Transactions on an Platform Level

We can also implement this strategy on a platform level, therefore, from the point of view of the application developer, implicitly handling the recording of undo operations.

# The abstraction
class Transaction:
def __init__(self, store):
self.store = store
self.undos = []

def set(self, key, value):
self.undos.append(('SET', key, self.store.get(key)))
self.store[key] = value

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
if type is not None: # An exception has occurred
self.abort()

def abort(self):
while self.undos:
o, k, v = self.undos.pop()
if o == 'SET':
if v is None:
del self.store[k]
else:
self.store[k] = v

# Example usage
store = {}
try:
with Transaction(store) as t:
t.set('key1', 'value1')
t.set('key2', 'value2')
# some error happens
raise Exception('Some error')
except Exception as e:
pass

Here, we move much of the code to a Transaction class, which is also a python context manager. Do you see the difference?

The example usage is failure-agnostic, you do not have to think about the possibility of failure to guarantee all-or-nothing semantics.

What’s the Big Deal™️

At this point you may ask yourself: “What’s the big deal?! All we did is move some code around”.

Technically, you are correct. In terms of Abelson and Sussman, we applied combination (#2) and abstraction (#3) and created the Transaction class.

However, the result of refactoring the code is significant. In essence, the transaction is a promise that you do not need to think about failure.

Now you get to code as if failure does not even exist

Conclusion

Abstractions have the power to reshape how we think about the world. For example, transactions have the power to reshape how we think about failure, that is, we don’t have to think about failure.

Alan Perlis famously stated that a language that does not affect the way you think about programming is not worth knowing.

Similarly, we may say that an abstraction that does not affect the way you think about the world in not worth knowing.

An abstraction that does not affect the way you think about the world in not worth knowing

--

--