# The Essence of Abstraction

Photo by Drew Dizzy Graham on Unsplash

*Abstraction*.

A word that we hear and speak all the time, and that is ever-present in discussions and technical exchanges among our peers.

A concept so fundamental, that it is at the root of almost every construct in software development, without which we would certainly be lost in a raging sea of complexity.

But what exactly *is* an abstraction?

One would rightfully think that the very people for whom this word is so dear and by whom it is so often used, when asked this question would promptly provide a clear answer.

But, alas, like many other terms with a similar status in software development, this is simply not the case.

*Stop and think for a minute*.

How would **you** define *abstraction*?

Has anyone ever given you a **precise** definition of it?

Although concepts can still be remarkably useful even when we lack a precise definition for them, there is a certain *power* that comes with tracing clear-cut boundaries around concepts.

Dealing with fuzzy concepts is like looking at an object through an unfocused lens: the object seems blurry and we can't quite make out its details, it's hard to tell where it begins and where it ends.

By making our definitions more precise, we're focusing our metaphorical lens and thus sharpening the images we see through it.

So let's do an in-depth exploration of this question, where by doing so we'll understand the **essence** of abstractions, their properties, their role in managing complexity and their relation with other ubiquitous concepts in software development.

## Intuition

This situation where we're familiar with a term or a concept yet we can't quite define it is fairly common in our daily lives. I call it the "I'll know it when I see it" phenomenon and it's particularly noticeable when there is some kind of aesthetics at play.

*Photo by R ARCHITECTURE on Unsplash*

Look at the picture above.

Although this may not be your preferred style of interior design, I believe we can agree that it is in some sense harmonious and well-designed.

Now, if you're like me and have no expertise in interior design, it's hard to tell what *exactly* makes it so.

It's remarkable that you could show me a hundred different pictures of interior design and in a few minutes I could easily tell which ones I like and which ones I don't, yet if you asked me to *produce* even a single example of a design that appeals to me, I would fail spectacularly.

Despite my inability to recognize the patterns that make a design appealing, *they are there*, and if I wanted to start recognizing these patterns, a reasonable course of action would be to examine several examples of good designs and look for what they have in common. This way, I would be able to separate the **essential** characteristics of a good design from the **incidental** ones.

So this is what we'll do with abstractions. We'll start by investigating some constructs that we intuitively recognize as abstractions, and then by gathering their commonalities we'll gradually refine and narrow down our definition.

### Functions

The first construct we'll investigate is the **function**, also known as procedure or subroutine.

Functions are ubiquitous in software development, you can find them in almost every programming language, and some people consider them such a fundamental concept that they've even designed an entire programming paradigm around it (functional programming).

A function is a parameterized block of code encapsulated as a single unit, which can then be called whenever we need to execute that block of code.

When functions first came into being, their primary purpose was to facilitate code reuse while preventing repetition (AKA copying and pasting code), and even nowadays when people first learn about functions, this is what is usually used as the motivation for their existence.

However, people quickly realized that this encapsulation has an interesting side effect: it creates a conceptual separation between **what** that block of code does and **how** it does it.

Think about the last time you used a function from a library or a framework, or even one that you implemented yourself a long time ago.

At that time, did you know **how** exactly that function was implemented, that is, what its **source code** looked like in detail? *Probably not*, yet I bet you were still able to use it effectively.

This idea is not exclusive to software development, much on the contrary, it's something we can find everywhere in our daily lives. We're surrounded by things we use all the time, yet we have no idea how they work.

Take a car for example: a lot of people drive cars, but how many of them know how they work? How many of them could "implement" a car, that is, build one from scratch?

Whether in our everyday lives or in software development, this separation between **what** something does and **how** it does it is key to a process we call **information hiding** or conceptual compression.

This process is crucial to managing complexity, as it allows us to reduce the amount of information one needs to deal with in a given context.

For example, the context in which we're **using** a function requires much less information about the function than the context in which we're **implementing** that same function, and these contexts are never the same, nor do they overlap.

Like when you call a hashing function, you probably don't really know what are the exact steps that are carried out to produce the hash, you only need to know that it produces a hash (and obviously, what a hash is).

Getting a little bit ahead of myself, all of these concepts will also make an appearance in constructs we'll investigate next, which is a pretty strong indicator that they're intimately related to the essence of abstraction and there's even a quote (which is one of my favorite characterizations of abstraction) that summarizes this idea:

The essence of abstraction is preserving information that is relevant in a given context, and forgetting information that is irrelevant in that context. - John V. Guttag

Back to functions, I want to make this idea of information hiding more tangible through some diagrams.

For starters, considering that programs are sequences of instructions, we're going to represent each instruction as a circle, like so:

What exactly each of these instructions is doesn't really matter for our purposes, they could be a variable definition/assignment, some conditional, a function call, etc.

Each instruction carries some amount of information, which we have to keep in our heads when dealing with that instruction.

With that in mind, we can represent this amount of information with a number, like an "information score" if you will, which we'll represent with a box on the left side of each instruction:

According to this diagram, the resulting information score of our program is 8, which is the sum of the information scores of each instruction.

Now let's extract the instructions `I2`

and `I3`

as a function `A`

:

In our diagram, we now have a circle `A`

that represents the function we just extracted and the two instructions `I2`

and `I3`

are linked to it, to represent the idea that `A`

is **implemented** in terms of them.

As we mentioned before, using a function requires less information than implementing it, so in this case let's say that using `A`

requires 2 units of information (this was an arbitrary choice, in practice it depends on each function).

In this case, the block of code represented by `I1`

, `A`

and `I4`

, which previously used to have a combined information score of 8, now has a combined information score of 6, that is, we reduced the amount of information we need to deal with by 2 units.

Here's another example:

In this case, functions are already extracted and we have more than one "layer" of code, but the idea remains the same:

The information score associated with the implementation of `A`

is 7, but the information needed to use it has a score of only 3.

`B`

is implemented in terms of `A`

and `I3`

, so its implementation has an information score of 4, which is the sum of the information score of `I3`

and the information score associated with the **usage** of `A`

.

When analyzing these "layers" of code, we see that many times, functions are implemented in terms of other functions, which ultimately means that the "what" of a function *is part of* the "how" of another.

By breaking up our program into functions, we can limit the amount of information we need to deal with at any given time, that is, we **keep complexity in check**.

Another important observation is that what a function does is **always implied** by how it does it, in other words, the "how" of a function must always **support** its "what". Another way of looking at this is that if you read the source code of a function you should be able to infer what it does.

The converse, however, is **not** true, that is, a function's "what" does **not imply** its "how", in the sense that just by looking at what a function does, you can't infer how exactly it is implemented, which is precisely what supports the phenomenon of information hiding.

Although a function's "what" does not imply its "how", it **constrains** it, in the sense that the "how" must always be **compatible** with the "what". This relationship is crucial to yet another useful side-effect that arises from this separation between "what" and "how" of a function, which is **decoupling**.

**Coupling** is a measure of how much changes to something affect other things that depend on it.

For example, if you have a function `A`

that is implemented in terms of (that is, that makes use of) another function `B`

, changes to `B`

*might* affect `A`

, depending on the extent and nature of those changes.

If we only change how `B`

works but we keep what it does "intact", then `A`

will be unaffected by those changes, as `A`

does not really care, nor it depends (at least ideally) on the internals of `B`

.

If, however, we change what `B`

does, then `A`

will be affected by those changes.

By carefully selecting what kind of information we'll *expose* through a function's "what", we can control the extent to which changes to its "how" will affect those who depend on it.

### Objects

Next, we'll move to objects.

Objects are amalgamations of data and operations that are closely related to that data, and they serve a lot of different purposes like:

Collocating data and behavior for cohesion.

Facilitating contextual data sharing among operations.

Enforcing data consistency/validity (AKA invariants).

Concealing the data's internal representation as a decoupling mechanism.

Of these purposes, we're going to focus on the last one as it is the most relevant to our exploration.

In contrast to plain data records, where the data is accessed directly by clients without any kind of mediation, objects allow us to *conceal* the actual data stored in them and to expose a **representation** of that data that **does not necessarily map 1-to-1** to it.

For instance, suppose we have a record that represents a person, and that it has 3 data fields: `firstName`

, `lastName`

and `dateOfBirth`

.

When using that record, clients have no choice but to read and write directly to these fields.

If we use an object, however, instead of giving free access to clients to these data fields, we could expose only two methods: `getName`

and `getAge`

, where `getName`

returns the first name concatenated with the last name and `getAge`

returns the elapsed time in years between the person's date of birth and the current time.

Although the object's *internal data representation* would be the same, that is, it would still have the same three data fields internally, both reads and writes would always be **mediated** by the object's methods, so that only a **particular representation** of that data would be known by clients.

Notice that once again the concept of **information hiding** makes itself present, where besides encapsulating **behavior** just like functions do, objects also encapsulate **state**.

This encapsulation gives rise to a division that splits an object into two parts: the **interface** and the **implementation**.

An object's interface is composed of the data fields (if any) and methods it exposes, it characterizes the object's **behavior**, how it can be interacted with and what can be expected from it.

An object's implementation is composed of the object's internal data representation and its inner workings, which includes its methods implementations as well (which is why we consider that objects also encapsulate behavior).

The purpose of this encapsulation is twofold: it reduces the amount of information clients need to know in order to interact with the object and it decouples clients from the object's implementation details so that changing the implementation in a way that's compatible with the interface does not affect clients.

Much in the same way that a function's "how" implies its "what", an object's implementation always implies its interface, in the sense that the information that's present in the interface is always a **subset** of the information that the implementation contains.

And although an object's interface **constrains** its implementation, it does **not imply it**, as there are lots of different implementations that are compatible with the same interface.

For instance, going back to our example where we have an object that represents a person, the reason it's possible to have a `getAge`

method in its interface is because although there's no `age`

field in its implementation, there's a `dateOfBirth`

field from which the person's age can be **derived**.

On the other hand, if the situation was reversed, that is, if instead of having an internal `dateOfBirth`

field, the object had an `age`

field instead, there would be no way for the interface to expose a `getDateOfBirth`

method, as **it's not possible to derive** a person's **exact** date of birth (year, month, day, hour and minute) just from the person's age.

#### About Interfaces

Before we move on to the next construct, I want to talk a little bit about the term "interface" as it is a term with a very broad meaning and that has been heavily overloaded with time.

Depending on each person's background, this term will mean a completely different thing, which will make things confusing.

For a person that comes from a Java or a C# background, "interface" could mean a specific **keyword** that exists in the language and that defines a sort of a "blueprint" for objects, in terms of what methods they should have along with the signature of these methods.

`interface Bicycle { // wheel revolutions per minute void changeCadence(int newValue); void changeGear(int newValue); void speedUp(int increment); void applyBrakes(int decrement);}`

`interface IPoint{ // Property signatures: int X { get; set; } int Y { get; set; } double Distance { get; }}`

For someone with a C++ background, "interface" could mean a class that has only pure virtual methods in it.

`class IBicycle { public: virtual void changeCadence(int newValue) = 0; virtual void changeGear(int newValue) = 0; virtual void speedUp(int increment) = 0; virtual void applyBrakes(int decrement) = 0;}`

For people in the Typescript land, "interface" could mean a keyword that defines a type.

`interface Bicycle { changeCadence: (newValue: number) => void; changeGear: (newValue: number) => void; speedUp: (increment: number) => void; applyBrakes: (decrement: number) => void;}`

And so on.

So, to avoid confusion, I want us to have an agreement on a very specific meaning for this term, so that we all mean the same thing when we use it.

Although all of the above meanings share some common characteristics with the meaning I intend for this term, all of them focus too much on the **"shape"** of things — they specify what methods or attributes some object should have, but not necessarily their **behavior**.

For example, there are a million different ways to implement the `Bicycle`

interface in Java, and although these implementations could all be compatible with the interface shape-wise, they could all do completely different things. The fact that two objects have the same methods and these methods have the same signature, it doesn't mean that their behavior is compatible.

So, in the context of this post, whenever you see the word "interface", I want you to think about **behavior**, about the **properties** that the interface expects from a given implementation. And by properties, I **do not mean data fields or attributes**, but rather any characteristic that can be attributed to something.

Notice that when using the term "interface" this way, interfaces give us a much *stronger* guarantee than when using the term to merely signify a "shape" that something should comply with.

Many times, the behaviors/properties that an interface establishes **cannot be captured** by the type systems that are usually available. For example, an interface for a `Sorter`

object could establish that implementations must have a time complexity of $\mathcal{O}(n\log(n))$, but this kind of property usually cannot be enforced by type systems.

Or maybe there's a `Widget`

interface that specifies a `render`

method that must be **pure** (i.e. should always yield the same result for the same parameters and should not have side-effects), which is also not something that's enforceable through a language features alone.

To reiterate, from now on, "interface" refers to the set of **all** properties objects that implement that interface must have, including those that cannot be enforced by the language's type system, or even the language's runtime.

### Application Programming Interfaces (APIs)

Before we start talking about APIs, there's an important discussion we need to have regarding **terminology**.

The term **API** is a very broad one and can assume a specific meaning depending on the context and the demographic you're dealing with.

Nowadays, if you're a web developer, the first thing that comes to mind when you hear the term API is probably an HTTP API, which is composed of a set of endpoints that move data around using JSON, or less commonly, XML.

However, the term is much older than HTTP APIs and in its broadest sense signifies a set of operations that are exposed by a system so that other systems can interact with it.

So when we talk about APIs in general, we might be talking about the set of functions and classes that a library or framework exposes, or we might be talking about how a database lets you interact with it, or about the commands a command-line interface accepts, and so on.

Moving forward, when talking about APIs, we'll adopt the term in its broadest sense.

Much like functions and objects, APIs rely heavily on the notion of **information hiding**, and they encapsulate both behavior and state.

For example, consider an HTTP API. Behind this API there might be a lot of stuff going on, like reverse proxies, load balancers, servers, caches, databases, etc.

However, when you're interacting with this API, you don't need to know about any of that, you only need to know what endpoints are available and what data you need to send to them to get the desired behavior.

Much like functions and objects, this encapsulation has some important consequences:

First, by hiding their inner workings, APIs greatly reduce the amount of information that clients need to know in order to use them.

Second, this encapsulation breaks up APIs into two "parts", a **contract** which is what can be expected from an API in terms of behavior and the API's **internals** , which is composed of all its implementation details, where the internals always **implies** the contract.

Third, although an API's contract establishes limits on the API's internals, it does **not imply it**, in the sense that just by looking at an API's contract, you cannot infer how exactly it works internally.

Lastly, this encapsulation **decouples** clients from the APIs internals, hiding these implementation details behind the contract, which is what clients ultimately depend on, so that changing the internals of an API does not break clients as long as these changes are compatible with the existing contract.

## Defining Abstraction

Now that we've explored a few constructs that we intuitively recognize as abstractions, let's take a step back, look at what they have in common and come up with some generalizations.

In all constructs we investigated, a recurring theme was **information hiding**, where this phenomenon was always a result of some kind of encapsulation.

This encapsulation divided each construct into two parts — functions were divided into a **"what"** and a **"how"**, objects into an **interface** and an **implementation** and APIs into a **contract** and its **internals**, and although these parts were given different names for each construct, they are **analogous** and it's easy to notice the correspondences among them.

Then, we saw that one of these parts is *external* and the other is *internal*, where the external part always contains **less** information than the internal one, and it's the one that clients depend on.

Because of that, using the construct always requires **less** information than **building** it, which helps to keep complexity in check by preserving information that is relevant to clients while hiding information that is irrelevant to them.

Additionally, the internal part always **implies** the external one, where the converse is not true.

Lastly, because clients depend on the external part, they are **decoupled** from the internal one, so that changes to the internal part that are compliant with the existing external part, do **not** cause clients to break.

With these things in mind, the following definition arises:

An abstraction is a construct that offers a simplified representation of itself, where this representation exposes only a selected subset of the construct's properties.

Also, I propose we generalize the names of the external and internal parts that compose an abstraction, by calling them **interface** and **implementation** respectively, just like we do for objects.

This specific was made on the grounds that these terms are widely known and are commonly used to refer to these parts in general.

With that in mind, we can slightly complement our definition as follows:

An abstraction is a construct that offers a simplified representation of itself, where this representation, the **interface**, exposes only a selected subset of the construct's properties, the **implementation**.

Although this definition covers every characteristic we've seen so far, there's still room to make it more precise.

To do that, however, we'll need two things:

First, we'll talk about **properties** in depth, as they are at the core of our definition of abstraction and they are intimately related to the notion of information hiding.

Second, we'll have to up our game because natural language **won't suffice** as it lacks the precision we're looking for, so we'is information anyway?e world's most precise language: *Mathematics*.

### A Detour Into Properties

So far, we've been talking a lot about information and the notion of information hiding through encapsulation, but what *exactly* is information anyway?

In the context of abstractions, the answer is simple:

Information is a set of properties.

Previously, we've been using the idea of information because it was easier to deal with intuitively, as it was our initial setting, but whenever we referred to information we were always *implicitly referring to a set of properties*.

So, from this point onwards, we'll start using the notion of property instead of the notion of information, as the former will help us in our goal of making things more precise.

#### Defining Properties

Generally speaking, a property is some characteristic that can be attributed to something.

When we say that something is large, that thing has the property of being large, when we say that something is blue, that thing has the property of being blue, and so on.

Properties might have a description that is as simple as "being blue", or as complex as "being a function that given a prime number takes 3 times more time to run than when given a non-prime number", and beyond.

Each thing has a set of properties that **uniquely** describes it, and if two things have the exact same set of properties, then they are the same thing. Conversely, if two things are different, then there must be at least one property that one has and the other doesn't. This principle is known as the **Identity of Indiscernibles**.

Properties are easy to grasp intuitively, however, they are quite abstract and it's hard to refer to them directly. Thus, we usually refer to them through **predicates** instead, as we did a moment ago, and by predicates, I mean a sentence or part of a sentence that *describes* the property we're referring to.

Although it might seem like predicates and properties are the same thing, **that's not the case**, because just as there are many different ways of describing the same thing, there are many different predicates (actually infinitely many) that refer to the same property.

Even though this distinction will not be that relevant for the upcoming discussions, I just wanted to let the more attentive reader know that this was not a detail that was overlooked, as we'll be using predicates and properties interchangeably.

So let's see a few statements that illustrate this idea:

The number 2 is even.

The sky is blue.

Pigs don't fly.

The function

`double`

multiplies a number by 2.The function

`double`

takes a single argument.The function

`double`

is implemented by adding a number to itself.The

`GET /users`

endpoint returns a list of users.The

`GET /users`

endpoint never modifies the list of users.The

`GET /users`

endpoint cannot be called more than 100 times per minute by a single IP.The

`Person`

class has an`age`

field.The

`Person`

class is guaranteed to never have a negative age.The

`Person`

class has a`getAge`

method.The

`Person`

class doesn't have a`getDateOfBirth`

method.The function

`logout`

can only be called after the function`login`

has been called.

Each one of the statements above contains a predicate that refers to a specific property.

Going to back to the idea that each thing has a set of properties that uniquely describe it, if we take, for instance, an arbitrary **function**, there's a set of **all** the properties this function has and that describes **each little thing about it**, which corresponds to the **entirety** of the **information** that exists about this function, including **what it does** and **how it does it**.

The same idea is true for any other construct, be it an object, an API, or pretty much anything else.

#### Properties and Implication

Moving forward, I want to draw your attention to a key aspect of properties, which is the fact that a **set of properties implies other properties**.

In other words, if something has a certain set of properties, the properties contained in this set, together, imply other properties that are not necessarily part of the initial set.

For instance, if a cellphone has the property of having a 4G connection, and it also has the property of being within the coverage area of a 4G network, then, by necessity, it also has the property that it can connect to the internet without using wifi.

In another example, if the function `double`

has the property that it takes a single argument, then, by necessity, it also has the property that it **doesn't** take two arguments.

This notion of implication is going to be important for two reasons:

First, when we talked about an abstraction's **implementation implying its interface**, it was precisely this notion we were referring to.

Second, it allows us to **compare** sets of properties in terms of how much information they possess.

Let's delve into this second point.

Intuitively speaking, the amount of information a set of properties has, is **directly proportional** to how many properties are contained in it, so, the "bigger" the set, the more information it has and vice versa.

In reality, however, the situation is not that straighforward, as even if a set of properties is *properly contained* in another set, they could have the **same** amount of information. Similarly, adding more properties to a set, does not necessarily equate to adding more information to it.

The situation is analogous to solving systems of linear equations.

When solving these systems, each equation is a "piece of information", where the more pieces you accumulate, the fewer degrees of freedom your solution will have, and if you have just enough pieces, your solution is a single vector.

However, adding more equations to a system does not necessarily equate to adding more information, in the sense that some equations are "redundant", that is, the information they carry is **already implied** in previously existing equations in the system (more specifically, when an equation is a linear combination of the other equations).

The same thing happens for sets of properties, where adding properties to a set that are **implied** by that set **does not add more information** to it.

With that in mind, this implication relation between sets of properties establishes a way to compare them regarding "how much" information they have, as follows:

If two sets imply each other, then they have

**exactly the same amount of information**.If one set imply another, and the second

**does not imply the first**, then it has**more information**than the second.If one set is implied by another, and the first set

**does not imply the second**, then it has**less information**than the second.If a set does not imply another set, nor the second implies the first, then

**there is no way to compare them**in terms of "how much" information they have.

If we recall that an abstraction's **implementation** always **implies** its **interface**, then the notion of information hiding arises naturally, as this implication ultimately means that the **interface** has **less** information than the **implementation**.

Considering that we'll be talking a lot about properties, sets of properties and implications between them, I want to introduce some notation that will make the whole process much easier.

For individual properties, we'll use lowercase greek letters, like $\alpha$, $\beta$, $\gamma$, etc.

For sets of properties, we'll use uppercase greek letters, like $\Gamma$, $\Delta$, $\Sigma$, etc.

And for the implication relation, also called consequence relation, we'll use the turnstile symbol $\vdash$.

So, if a set of properties $\Gamma$ implies a property $\alpha$, we'll write $\Gamma \vdash \alpha$.

If a set of properties $\Gamma$ implies a set of properties $\Delta$, we'll write $\Gamma \vdash \Delta$.

And so on.

There are a few important properties of the consequence relation $\vdash$ that are worth mentioning:

$\vdash$ is

**reflexive**, which means that for any property $\alpha$, $\alpha \vdash \alpha$, that is,**every property implies itself**.$\vdash$ is

**transitive**, which means that for any properties $\alpha$, $\beta$ and $\gamma$, if $\alpha \vdash \beta$ and $\beta \vdash \gamma$, then $\alpha \vdash \gamma$.$\vdash$ is

**not symmetric**, which means that if $\alpha \vdash \beta$,**not necessarily**$\beta \vdash \alpha$.

#### Deductive Closure

Let's say we have a set of properties $\Gamma = \{ \alpha_1, \alpha_2, \alpha_3, ... \alpha_n \}$.

Then, suppose we start adding other properties to it that are **implied** by $\Gamma$, that is, we add a property $\alpha_{n + 1}$ such that $\Gamma \vdash \alpha_{n+1}$, then a property $\alpha_{n + 2}$ such that $\Gamma \vdash \alpha_{n + 2}$ and so on, infinitely, until we end up with a set we'll call $\Gamma^*$ that has **all the properties that are implied by $\Gamma$**.

This set has two interesting properties:

First, this set is maximal, in the sense that it is the "biggest" set of properties that has **the same amount of information** of $\Gamma$.

Second, because it has all the properties that are implied by $\Gamma$, any property $\phi$ that is **not in this set** is not implied by $\Gamma$, and thus, has **some information that $\Gamma$ itself does not have**. This means that adding $\phi$ to $\Gamma$ (or $\Gamma^*$) will necessarily **increase** the amount of information it has.

The set of all properties $\Gamma^*$ implied by a given set $\Gamma$, is called the **deductive closure** of $\Gamma$.

Another way to think about the deductive closure of a set of properties, it through an analogy using train stations.

Let's say there's a set of train stations and a relation between those stations that tells us whether from a station $\alpha$ you can get to another station $\beta$ with **any** number of connections.

The "connection closure" of this set of stations is the set of all stations that you can get to from any station in the original set, that is, the set of all stations that are connected to the original set.

If we replace stations with properties and the connection relation with the consequence relation $\vdash$, this "connection closure" is analogous to the deductive closure.

Now let's explore a few properties of deductive closures.

As $\Gamma^*$ is the set of all properties that are implied by $\Gamma$, it follows that $\Gamma \vdash \Gamma^*$.

Because the consequence relation $\vdash$ is reflexive, the deductive closure $\Gamma^*$ of a set $\Gamma$ contains $\Gamma$ itself, that is, $\Gamma \subseteq \Gamma^*$.

Also, there's an important theorem that relates implication between sets of properties and their containment relationship, and it states that for any two sets of properties $\Gamma$ and $\Delta$, $\Delta$ implies $\Gamma$, if and only if the deductive closure of $\Delta$ contains the deductive closure of $\Gamma$, that is, $\Gamma^* \subseteq \Delta^*$.

First, let's understand this theorem intuitively and then we'll prove it.

What this theorem tells us is that a set of properties that are implied by another set of properties, is always a **selection** of properties of the second set.

Conversely, anytime you **select** some properties from a set of properties, that subset you just carved from the original set **is implied** by the first set.

Considering that an abstraction's implementation always implies its interface, it follows that the interface is always a **selection** of the properties contained in the implementation.

Conversely, anytime you take an implementation and **select** some properties of it, this selection is **implied** by the implementation and thus it is a suitable interface.

Notice that we abused the language a little bit here, because where the theorem talks specifically about the deductive closure of sets of properties regarding their contaiment relations, we simplified things and talked about the sets of properties themselves.

The main reason we introduced the concept of deductive closure (besides being an interesting concept in itself), was to "justify" this abuse.

Intuitively, one might believe that:

But that's not really the case, as something like this could happen:

In this case, as $\alpha \vdash \beta$, then $\Delta \vdash \Gamma$, however, it's cleary not the case that $\Gamma \subseteq \Delta$.

So, for us to establish the desired relation while retaining the soundness of our reasoning, we have to consider the deductive closure of sets.

Now let's prove this theorem.

First, let's prove that if $\Delta$ implies $\Gamma$, then $\Gamma^* \subseteq \Delta^*$.

Suppose that $\Delta$ implies $\Gamma$, that is, $\Delta \vdash \Gamma$.

Then, let's take an arbitrary property $\phi$ in $\Gamma^*$.

By definition, if $\phi$ is in the deductive closure of $\Gamma$, which is $\Gamma^*$, then it means that $\Gamma$ implies $\phi$, that is, $\Gamma \vdash \phi$.

As the consequence relation $\vdash$ is **transitive**, and we know that $\Delta \vdash \Gamma$, then it follows that $\Delta \vdash \phi$.

By definition, this means that $\phi$ is in the deductive closure of $\Delta$, that is, $\phi$ is in $\Delta^*$.

As $\phi$ was an arbitrary property in $\Gamma^*$, this means that for any property $\phi$ in $\Gamma^*$, $\phi$ is also in $\Delta^*$, which means that $\Gamma^* \subseteq \Delta^*$. QED.

Now let's prove the reciprocal, that is, if $\Gamma^* \subseteq \Delta^*$, then $\Delta \vdash \Gamma$.

Suppose that $\Gamma^* \subseteq \Delta^*$.

Then, let's take an arbitrary property $\phi$ in $\Gamma$.

As we saw before, a set of properties is always contained in its deductive closure, therefore $\Gamma \subseteq \Gamma^*$.

This means that $\phi$ is also in $\Gamma^*$.

Because we know that $\Gamma^* \subseteq \Delta^*$, then it follows that $\phi$ is also in $\Delta^*$.

By definition, because $\phi$ is in the deductive closure of $\Delta$, that is, $\phi$ is in $\Delta^*$, then it means that $\Delta$ implies $\phi$, that is, $\Delta \vdash \phi$.

As $\phi$ was an arbitrary property in $\Gamma$, this means that for any property $\phi$ in $\Gamma$, $\Delta$ implies $\phi$, which means that $\Delta$ implies $\Gamma$, that is, $\Delta \vdash \Gamma$. QED.

#### Ordering Sets of Properties

Now that we established a connection between implication among sets of properties and their containment relationship, we can use that connection to define an ordering between sets of properties.

Before we do that, let's understand what an ordering is, and the different types of orderings that exist.

Intuitively speaking, an ordering is any relation that tells us how to compare two things, in terms of which is "smaller" and which is "bigger".

For a relation $<$ (which is not necessarily to be taken in the usual meaning of this symbol) to be considered an ordering, it needs to satisfy two properties:

**Transitivity**: If $a < b$, and $b < c$, then $a < c$.**Asymetry**: If $a < b$, then it is**not**the case that $b < a$.

There's the possibility to "exchange" **asymetry** for **antisymetry**, which states that if $a < b$, and $b < a$, then $a = b$. In this case, "smaller" actually means "smaller or equal".

When we have an ordering that is **asymmetric**, it is called a **strict ordering**, and when we have an ordering that is **antisymmetric**, it is called a **non-strict ordering**.

Then, we can subdivide orderings into two other categories: **total** and **partial**.

A **total** ordering, which is also called a **linear** ordering, is an ordering where **every** pair of elements can be compared, that is, it satisfies the **trichotomy** property, which states that for any two elements $a$ and $b$, either $a$ is smaller than $b$, $b$ is smaller than $a$, or $a$ and $b$ are the same thing.

Total orderings are the ones we're most familiar with, because that's how numbers are ordered with regards to the $<$ or $\leq$ relations.

They are called **linear**, because they form a "line", in the sense that each element "divides" the line into two sections, a section where elements are smaller than it, and a section where elements are bigger than it.

A **partial** ordering, on the other hand, is an ordering where **not every** pair of elements can be compared.

A concrete example of a partial ordering is the "descends from" relation in a family tree:

In the above picture, arrows depict the "descends from" relation, and as you can see, not every pair of elements are **connected** by an arrow, which means that those elements are **not comparable**.

For instance, neither $a$ descends from $b$, nor $b$ descends from $a$, therefore $a$ and $b$ are not comparable.

However, it's still an ordering (and in this case, a strict one), as it satisfies both asymmetry, as if someone descends from another person, then that person doesn't descend from the first one, and transitivity, as if a person descends from another person, and that person descends from yet another person, then first descends from the last.

Back to sets of properties and their relations, taking into account that we were using the consequence relation $\vdash$ to compare sets of properties and their information contents, naturally, one is led to believe that $\vdash$ could be used to order sets of properties.

But alas, that's not possible, because although $\vdash$ is transitive, it is **not** asymmetric nor antisymmetric.

For instance, let's say that there are two different properties $\alpha$ and $\beta$ such that $\alpha \vdash \beta$ and $\beta \vdash \alpha$.

In this case, taking $\Gamma = \{ \alpha\}$ and $\Delta = \{ \beta\}$, it follows that $\Gamma \vdash \Delta$ and $\Delta \vdash \Gamma$, which already shows us that $\vdash$ is not asymetric.

Also, as $\alpha \ne \beta$, then $\Gamma \ne \Delta$, which means that $\vdash$ is also **not** antisymmetric.

However, there's a small adjustment that we can make to turn $\vdash$ into an ordering relation.

Although $\vdash$ is a not suitable ordering for **sets of properties**, it **is** a suitable ordering for the **deductive closures of sets**, that is, for the sets that are **closed under deduction**.

When we consider only deductive closures of sets, $\vdash$ is still transitive, but now it also becomes antisymmetric, as we'll prove next.

Suppose that $\Gamma^* \dashv \vdash \Delta^*$.

Then, let $\phi \in \Gamma^*$ be an arbitrary property of $\Gamma^*$.

As $\Delta^* \vdash \Gamma^*$, then in particular, $\Delta^* \vdash \phi$.

By the definition of $\Delta^*$, it follows that $\phi \in \Delta^*$.

As $\phi$ is arbitrary, then $\Gamma^* \subseteq \Delta^*$.

By replicating this reasoning in an analogous way for $\Delta^*$, then $\Delta^* \subseteq \Gamma^*$.

As two sets are equal precisely when one contains the other and vice versa, then $\Delta^* = \Gamma^*$. QED.

Thus, by restricting ourselves to deductive closures of sets of properties, $\vdash$ becomes a **non-strict partial ordering**.

The reason it is **partial** and not **total**, is because it is **not** the case that for any two sets of properties $\Gamma^*$ and $\Delta^*$, that either $\Gamma* \vdash \Delta^*$ or $\Delta^* \vdash \Gamma^*$, that is, some sets of properties are **not comparable** by $\vdash$.

With that, we have a formal notion of ordering sets of properties according to the amount of information they have.

### A Formal Definition

Now that we have a thorough understanding of properties, we can come up with a formal definition for the concept of abstraction.

Without further ado, I propose the following definition:

An abstraction is an ordered pair $\langle \Gamma, \Delta \rangle$ such that $\Gamma$, called "the interface", and $\Delta$, called "the implementation", are **sets of properties** such that **$\Delta \vdash \Gamma$**.

Now I want to show you that this definition captures all the intuitive notions we have about abstractions.

First, it captures the notion that the abstraction is composed of two parts, the interface and the implementation, which are represented by $\Gamma$ and $\Delta$ respectively.

Second, it captures the notion of information hiding, as $\Delta \vdash \Gamma$, which tells us that $\Delta$ has $\Gamma$ has **less** information than $\Delta$. Alternatively, recalling that if $\Delta \vdash \Gamma$ then $\Gamma^* \subseteq \Delta^*$, the information that the interface has is quite literally a **subset** of the information the implementation has, where the information that is being hidden through the process of encapsulation is
**precisely the difference $\Delta^* - \Gamma^*$**.

Third, it captures the notion that the implementation always implies the interface, which is laid out in the definition itself.

Lastly, it captures the notion that although the interface does not imply the implementation, it **constrains**, given that $\Gamma^* \subseteq \Delta^*$, which means that $\Delta^*$ can only "shrink" to the point that it still contains $\Gamma^*$.

Now that we have a formal definition of abstraction, let's explore some of its properties and relations to other concepts.

## The Three Personas

Three personas exist for every abstraction: the **client**, the **designer** and the **implementer**.

These personas are not necessarily different people, as many times they're actually the same person. The idea is that each persona personifies a different **role** or **perspective** for an abstraction.

The **designer** is the persona who **designs** the **interface**, by *deciding* which properties it will be composed of.

The **implementer** is the persona who **implements** the interface, it crafts the **implementation** according to the *constraints* imposed by the **interface**.

The **client** is the persona who **uses** the abstraction, where its **use cases** must be *supported* by the **interface**.

When we look at our formal definition of abstraction, we notice that the **designer** is related to $\Gamma$ and the **implementer** is related to $\Delta$, but what about the **client**, what does it relate to?

To answer this question, we introduce the concept of **use case**.

A **use case**, within the scope of this post, is a set of properties $\Sigma$ that represents the **requirements** a **client** needs the abstraction (and more precisely, its interface) to fulfill in order to **support** that use case.

To understand use cases in full, first, we need to pay attention to the fact that most times, clients **do not rely on all** the properties that an abstraction exposes through its interface, but only a **subset** of them.

Let me show you a concrete example:

Suppose there's a function `double`

that outputs two times the number it receives as its input.

Besides the fact that this function `double`

multiplies numbers by two, it also has the property that it works for **any** number, and this property, of course, it part of its interface.

Also, let's say that in a given part of an application, we're calling that function three times: `double(2)`

, `double(4)`

, `double(7)`

.

At first look, it might seem that as a client, we're relying on the property that this function works for **any** number, but upon a close look, the property we need is **weaker** than that — what we actually need, is for the function to work for only three values, namely, `2`

, `4`

and `7`

.

The property that guarantees that `double`

works for the specific values `2`

, `4`

and `7`

, is **implied** by the property that it works for any number, however, the converse is not true.

Thus, formally speaking, the properties we really need, represented by $\Sigma$, are **implied** by the properties that the interface, $\Gamma$, contains, that is, $\Gamma \vdash \Sigma$.

Also, from the theorem we saw before, $\Sigma^* \subseteq \Gamma^*$, that is, the properties we're using are a **subset** of the properties contained by the interface.

So, by taking the **use case**, the **interface** and the **implementation** all at once, we arrive at the following conclusions:

The implementation implies the interface, which in turn, implies the use case.

The use case is a subset of the properties of the interface, which in turn, is a subset of the properties of the implementation.

Having the concept of **use case** established, we can now explore the interactions between these personas.

### Abstraction vs Flexibility

So far, we have thought about abstractions in a mostly **static**, where use cases, interfaces and implementations are "fixed".

Now, I want us to consider what happens when these things **change**, and how these changes affect each other.

Let's start by looking at changes to interfaces.

As we just saw, interfaces both support use cases and constrain implementations, a fact that is formalized by $\Delta \vdash \Gamma \vdash \Sigma$.

So, for a given interface $\Gamma$, there is the **set of all the use cases it supports** and also the **set of all suitable implementations**.

To formalize these concepts, we'll introduce two families of sets:

A family $\mathcal{S}(\Gamma)$ that represents the set of all use cases a given $\Gamma$ supports.

And another family $\mathcal{D}(\Gamma)$ that represents the set of all suitable implementations for a given $\Gamma$.

There are many different ways interfaces can be changed, and anytime an interface $\Gamma_1$ changes to a different interface $\Gamma_2$, the sets of supported use cases and suitable implementations will change as well.

However, I want us to focus in two specific kinds of changes, one where the interface **grows**, that is, $\Gamma_1 \subseteq \Gamma_2$ and another where the interface **shrinks**, that is, $\Gamma_1 \supseteq \Gamma_2$.

First, let's consider this situation intuitively.

For a given interface $\Gamma$, any use case $\Sigma$ must always be a **subset** of the properties of the interface, that is, $\Sigma^* \subseteq \Gamma^*$, therefore, as the interface **grows**, the **amount of possible subsets grows as well**. So, the more guarantees an interface has, the more use cases it supports.

Conversely, as the interface **shrinks**, the **amount of possible subsets of that interface shrinks as well**. So, the less guarantees an interface has, the less use cases it supports.

For instance, let's say there's a function `random(x)`

that returns a pseudo-random number between 0 and a given number `x`

.

If this function, at first, supports only **integers**, any use cases that would require passing **floats** are not supported by this function.

If we **augment** the function's interface so that it starts supporting **floats** as well as **integers** (and, of course, change its implementation accordingly), it will still support all of the previous use cases, but now it will also support the additional use cases that require passing **floats**.

When looking at implementations, however, the situation is the inverse.

For a given interface $\Gamma$, any suitable implementation $\Delta$ of that interface must be a **superset** of the properties that are guaranteed by $\Gamma$, that is, $\Gamma^* \subseteq \Delta^*$.

As $\Gamma$ **grows**, it **fits inside fewer implementations** than it did before, and thus there will be **fewer suitable implementations**.

As $\Gamma$ **shrinks**, it **fits inside more implementations** than it did before, and thus there will be **more suitable implementations**.

For instance, suppose an `Array`

object has a `map`

method that creates a new array from the original one by iterating on each element and mapping it to a new element using a callback that's passed by the client.

If initially, the `map`

method has no guarantees in terms of the **iteration order** that is going to be used, the implementer may choose *whatever order it sees fit*. Maybe it will iterate in direct order, or maybe in reverse, or maybe even iterate over all even indexes then move to the odd ones, it doesn't matter.

However, if `map`

changes its interface so that now it follows a specific iteration order, all those other implementations that had a different iteration order won't be suitable anymore.

Thus, to sum it up:

As the interface **grows**, the set of all supported use cases **grows** and the set of all suitable implementations **shrinks**.

As the interface **shrinks**, the set of all supported use cases **shrinks** and the set of all suitable implementations **grows**.

Alternatively, we can say that the amount of supported use cases is **directly proportional** to the size of an interface and the amount of suitable implementations is **inversely proportional** to it.

What this tells us is that there is a **permanent tension** between **abstraction** and **flexibility**.

The **more abstracted** (or abstract) you make an abstraction, the fewer properties you guarantee, that is, the smaller the interface, which means that this abstraction becomes **less flexible**, as it supports fewer use cases.

On the other hand, the **more flexible** you make an abstraction so that it supports more use cases, the more properties you have to guarantee, that is, the bigger the interface, which means that this abstraction becomes **less abstracted** (or abstract), as it places more constraints on implementations.

At the same time, the **more abstracted** you make an abstraction, the less information its interface has, which means that clients need to know **less stuff** to understand and use the abstraction effectively.

Conversely, the **more flexible** you make an abstraction, the more information its interface has, which means that clients need to know **more stuff** to understand and use the abstraction effectively.

Balancing these two forces is something that we do all the time whether we're aware of it or not.

Framework and library maintainers have to choose carefully how abstract or flexible their APIs will be, for if abstractions are too abstract, they will cover far too few use cases of users.

If, however, they make their APIs too flexible and expose too much of its internals, it makes it hard to maintain and to evolve the APIs as changes to their implementations are more likely to break clients.

When choosing a library, or a service to use in our application, we'll be faced with this dilemma as well.

There are tools that cover a lot of use cases, and choosing such tools makes it easier for us to evolve our application as new use cases come by, however, these tools also require us to learn a lot to use them effectively.

Other tools are much simpler to use, as they abstract a lot of stuff for us, but at the same time, they are much more limited than other tools that are less abstracted, and thus as our application evolve, newer user cases might not be covered by these tools and we'll be forced to migrate to a more flexible abstraction.

Lately there was a lot of discussion on high code vs low code vs no code, and this idea of abstraction vs flexibility plays a key role in these discussions.

A no-code solution is usually easier to use than a low-code solution, which in turn is easier to use than a high-code solution.

However, this ease of use does not come for free, as often, the more a tool abstracts for you, the faster you hit its customizability ceiling, which is a common complaint regarding both low-code and no-code tools.

So now, it should be clear to us that the very thing that makes an abstraction easier to use and easier to maintain is also the thing that makes the abstraction less flexible, and vice versa, making an abstraction more flexible makes it harder to use and harder to maintain.

### The Tao of Abstraction

There's a phenomenon that occurs in some situations, where a **client** is simultaneously **a client and an implementer**, and an **implementer** is simultaneously **an implementer and a client**.

Consider an abstraction that's a function `map`

, and that function takes two arguments, an array and a **callback** that will be used to map the elements of the original array to a new, transformed array.

This is abstraction's interface is composed both by the fact that this function takes a callback as an argument, as well as **the interface that this callback must comply with**..

However, because this callback is **provided by the client**, and **not the implementer**, there's the possibility that this callback ends up being implemented **by the client itself** (think of passing an anonymous function).

Analogously, the implementer on the other side of the abstraction will receive this callback, but it doesn't have **access** to this callback's implementation, the only thing it can rely on is its **interface**, and thus it is a **client** of that callback.

## Stacking Abstractions

Almost every abstraction is built **on top of other abstractions**.

This **layering** of abstractions is a way to keep complexity in check, by ensuring that each abstraction has the "right" amount of information, both in its **interface** and in its **implementation**.

The amount of information contained in an **interface** is dictated by the **designer** of an abstraction.

Analogously, the amount of information that an **implementation** contains, is a consequence of how the abstraction's **implementer** chose to implement it.

Let's say there's an abstraction $A =\langle \Gamma_A, \Delta_A \rangle$ that is implemented in terms of (that is, its implementation makes use of) the abstractions $B = \langle \Gamma_B, \Delta_B \rangle$, $C =\langle \Gamma_C, \Delta_C \rangle$ and $D = \langle \Gamma_D, \Delta_D \rangle$.

What this means is that $A$'s implementation is a **client** of $B$, $C$ and $D$, and thus, it needs to **know** about their interfaces, as it relies on its properties. So, formally speaking, $\Gamma_B$, $\Gamma_C$, $\Gamma_D$ are all contained within $\Delta_A$.

By relying on $B$, $C$ and $D$, instead of having to know about $\Delta_B$, $\Delta_C$ and $\Delta_D$, $A$'s implementation **only** needs to know about the information that's contained in their interfaces.

### Responsibilities

In the context of software development, a **responsibility** is something that a construct must **do** or **know**.

From this definition, we can say that there are two kinds of responsibilities: the responsibility of **doing something**, and the responsibility of **knowing something**.

The responsibility of **doing** something, is a responsibility towards the **clients** of a construct, whereas the responsibility of **knowing** something is a responsibility towards the **construct itself**.

The reason we talk about responsibilities in software development is to keep complexity in check by **limiting** the amount of information we'll have to deal with at a time, which means that responsibilities are **tightly linked** with abstractions.

By carefully assigning responsibilities to constructs in software development, we can avoid constructs that either **do too much** or **know too much**.

From what we've learned so far, it's clear that responsibilities correspond to a certain kind of property, where responsibilities about **doing** something are part of an abstraction's **interface**, and responsibilities about **knowing** something are part of an abstraction's **implementation**.

Thus, assigning responsibilities, designing abstractions and layering them go hand in hand.

## Finale

At last, we have reached the end of our journey.

We started with questioning ourselves about what abstractions really are, and then little by little we progressed from intuition to a precise definition of abstraction.

Hopefully, I was able to show you the value of precision, and how sharp definitions leads to sharp thinking.

To finish this post, I want to make a rundown of the key takeaways we encountered throughout the post:

The essence of abstraction is

**information hiding**, where through a process of encapsulation, abstractions preserve information that is relevant in a given context, while forgetting information that is irrelevant in that context.Abstractions are composed of two parts: an

**interface**and an**implementation**, where the implementation**always implies**the interface, but the converse is**not true**.An abstraction is a construct that offers a simplified representation of itself, where this representation, the

**interface**, exposes only a**selected subset**of the construct's**properties**, the**implementation**.A property is any characteristic that can be attributed to something.

Information, in the context of abstraction, is essentially a

**set of properties**.Sets of properties may

**imply**other properties, which is represented by the consequence relation $\vdash$, which is**reflexive**and**transitive**.The

**deductive closure**of a set of properties, is the set of all properties that are**implied**by the initial set. E.g. $\Gamma^* = \{\phi \mid \Gamma \vdash \phi \}$.$\Delta \vdash \Gamma \leftrightarrow \Gamma^* \subseteq \Delta^*$.

We can use $\vdash$ to produce an

**ordering**for**deductive closures**that lets us compare sets of properties in terms of**how much information**they have, where $\Delta^* \vdash \Gamma^*$ means that $\Delta^*$ has**more information**than $\Gamma^*$.An abstraction can be formally defined as an ordered pair $\langle \Gamma, \Delta \rangle$ such that $\Gamma$, called "the interface", and $\Delta$, called "the implementation", are

**sets of properties**such that**$\Delta \vdash \Gamma$**, or, alternatively,**$\Gamma^* \subseteq \Delta^*$**.There are three personas associated with every abstraction: a

**client**, who**uses**the abstraction, a**designer**that**designs the interface**and an**implementer**, that comes up with the**implementation**.A

**use case**, is a set**$\Sigma$**of properties. For a use case to be**compatible**with an abstraction, its**interface**must**imply**that use case, that is, $\Gamma \vdash \Sigma$.$\Delta \vdash \Gamma \vdash \Sigma$ and $\Sigma^* \subseteq \Gamma^* \subseteq \Delta^*$.

For any interface $\Gamma$, there is a set $\mathcal{S}(\Gamma) = \{\Sigma \mid \Gamma \vdash \Sigma \}$ of all the

**use cases**that are supported by $\Gamma$ and a set $\mathcal{D} = \{\Delta \mid \Delta \vdash \Gamma \}$ of all the**implementations**that are suitable for $\Gamma$.There is a permanent tension between

**abstraction**and**flexiblity**, where the more abstracted you make an abstraction the less flexible it becomes, and vice versa.As the interface

**grows**, the**more**use cases it supports but the amount of suitable implementations**decreases**and the amount of**information**we need to know in order to use it**increases**. $\Gamma_1^* \subseteq \Gamma_2^* \rightarrow \mathcal{S}(\Gamma_1) \subseteq \mathcal{S}(\Gamma_2) \wedge \mathcal{D}(\Gamma_1) \supseteq \mathcal{D}(\Gamma_2)$.As the interface

**shrinks**, the amount of suitable implementations**increases**, and the amount of**information**we need to know in order to use it**decreases**, but the**fewer**use cases it supports. $\Gamma_1^* \supseteq \Gamma_2^* \rightarrow \mathcal{S}(\Gamma_1) \supseteq \mathcal{S}(\Gamma_2) \wedge \mathcal{D}(\Gamma_1) \subseteq \mathcal{D}(\Gamma_2)$.A responsibility is something that a construct must

**do**or**know**.Responsibilities that are about

**doing**something are part of an abstraction's**interface**and responsibilities that are about**knowing**something are part of an abstraction's**implementation**.Stacking abstractions is a way to make sure that abstractions do not accumulate more responsibilities than they should.

The End.