I've been musing about what separates abstraction from indirection in practice.
In practice what is referred to as "abstraction" is not so cut and dry. Abstraction in practice typically refers to "things" with two properties: naming and encapsulation of more primitive "things".
An example, using C++, is:
class Spout {
public:
Spout(float flow_rate) : flow_rate_(flow_rate) {}
float get_flow_rate() const {
return this->flow_rate_;
}
private:
// in ml/s
float flow_rate_;
};
class Handle {
public:
Handle(float length) : length_(length) {}
private:
// in cm
float length_;
};
class Teapot {
public:
Teapot(float height, float radius, Spout* spout, Handle* handle)
: height_(height), radius_(radius), volume_left_(0), spout_(spout), handle_(handle) {}
/**
* Fills the teapot to the brim.
*/
void fill() {
// Forumla to compute volume: pi * r^2 * h
this->volume_left_ = 3.14 * (this->radius_ * this->radius_) * this->height_;
}
/**
* Pours from the teapot, returning the amount poured.
*/
float pour(float seconds) {
float basis = this->volume_left_;
float delta = seconds * this->spout_->get_flow_rate();
this->volume_left_ = basis - delta;
if (this->volume_left_ <= 0.0) {
this->volume_left_ = 0.0;
}
return delta;
}
/**
* Returns remaninig volume
*/
float volume_remaining() {
return this->volume_left_;
}
private:
// cm
float height_;
// cm
float radius_;
// ml
float volume_left_;
// Teapots need spouts!
Spout const* spout_;
// Teapots also need handles... but what kind of handle?
Handle const* handle_;
};
The above shows: 1. Meaningful names. 2. Encapsulation of more primitive "things". "Things" in this instance are floating point values and other classes.
But the above is not abstraction, it is indirection.
Indirection isn't talked about much, and if so, in a limited context. Therefore, I can't definitively posit my argument is the norm. However, I desire to have indirection be an aspect of discussions about how solutions to problems are considered and carried out in the software trade.
Before going further, some definitions are in order.
1.a. disassociated from any specific instance
- withdrawn in mind : inattentive to one's surroundings
1.a. the act or process of abstracting : the state of being abstracted 1.b. an abstract idea or term
Note: "abstract" and "abstracted" are needed to defined "abstraction" hence their inclusion.
2.a. lack of straightforwardness and openness
Off the bat: how "abstraction" is expressed in practice is not what the word actually means. To give a concrete example of how "abstraction" is expressed in practice (yes, doing so is very meta) is usually with something like an interface (again, using C++):
class IPourer {
public:
virtual ~IPourer() {};
virtual int pour() = 0;
};
class IFiller {
public:
virtual ~IFiller() {};
virtual void fill() = 0;
};
The example is still very abstract. To make it "concrete":
class IPourer {
public:
virtual int pour() = 0;
};
class IFiller {
public:
virtual void fill() = 0;
};
class Teapot : public IPourer, public IFiller {
/* everything written before before */
};
Now we can take all the above and do something like:
#include <iostream>
class IPourer {
public:
virtual int pour() = 0;
};
class IFiller {
public:
virtual void fill() = 0;
};
class Spout { /* ... */ };
class Handle { /* ... */ };
class Teapot : public IPourer, public IFiller {
/* everything written before before */
};
void pour_and_log(std::ostream& out, IPourer* p, float seconds) {
float poured = p->pour(seconds);
out << "Poured(ml): " << poured << '\n';
}
int main(int argc, char** argv) {
// Pours at 8ml per second
Spout spout = Spout(8);
Handle handle = Handle(8);
Teapot teapot = Teapot(8, 8, &spout, &handle);
teapot.fill();
std::cout << "Remaining: " << teapot.volume_remaining() << '\n';
pour_and_log(std::cout, &teapot, 8);
std::cout << "Remaining: " << teapot.volume_remaining() << '\n';
pour_and_log(std::cout, &teapot, 6);
std::cout << "Remaining: " << teapot.volume_remaining() << '\n';
// So clean...
return 0;
}
The above is intended to illustrate my main point: the example, which is most likely what is in the wild, is indirection not abstraction. Chiefly, the distinction I have come to is: does the programming concept sit over or under concepts attempting to be conveyed? Mind you, this is concepts, not objects, data, etc.
If it sits over, it is an indirection, no matter how it is named or flavored.
If it sits under, it is an abstraction.
To give the example again, C++ the language is the abstraction. It is means to produce machine code. In fact, all language runtimes, even operating systems, are abstractions. They're providing an interface which is near impossible to extend for most programming jobs these days (looking at you, web dev).
I cannot conceive how C++ would adopt an abstraction to make teapots and abstraction of the language, but that is besides the point.
However, if you're a keen reader, you've probably thought, "but wait... aren't operating systems just an indirection over the hardware then?"
Yes... and no...
Yes it is an indirection, as there are interfaces (i.e., "open", "read", etc.) which make interacting with the hardware easier.
However, if needed, I can dip into the kernel and access the hardware directly. The interface is not a gate, it is a window, and I have keys to the front door around the other side of the house.
I'm still wrestling through the distinction, and I'm not entirely sure how to express this idea as code... yet!
In conclusion, given all the above, I'm lead to an arbitrary number of 99%. 99% of what is attributed to "abstraction" is merely "indirection". Wrapping SQL queries with a ton of application code is not abstraction. Trying to map out hierarchical data structures via inheritance is not abstraction. Even interfaces are not abstractions! They're concrete contracts, and conceptually "wrap" or "sit over" a concrete implementation.
An aside above about interfaces: what good is an interface, if it does not "connect" to anything? It isn't worth anything. It begs for a connection, nay, necessitates one. Interfaces require something concrete, and therefore, I'm wary of giving them the honor of being an abstraction mechanism. Sure, it they're useful, but they're not a silver bullet and can make products worse.
And most (not all) indirection is incidental complexity (i.e. your fault, thanks Rich!) and therefore needs to be approached with caution.
Class based design is not "design by abstraction" it is systemic indirection via named encapsulations all necessitating explicit manifestation at some point, even for "generic" data structures.
Interfaces are not abstractions, they are concrete indirections to concrete instances.
The industry writ large continues to practice concepts which are opposed to building simple -- and therefore reasonable -- systems. As a result, I've taken to talking about solutions (when engineering is the solution) as "meeting the minimum of complexity inherent" rather than "simple", since it doesn't seem the latter is achievable within most systems, and I have found few souls which seek an objective definition of "simple".
The corpus of "things that sit over" are indirections. The substrate which undergirds the expression of intent is an abstraction.
Most of the time, when software is involved, will be "meeting the minimum complexity inherent". One can call it "simple", but one can also be delusional (and yes, I'm speaking from experience).
Let us charge forth and create as little indirection as needed, and lean on tools which enable useful abstractions.
P.S.
I was chatting with a colleague about Clojure transducers. He mentioned Haskell's fusion property the compiler begets which exploits the pure functional nature of the language to eliminate allocations of intermediary processing steps. It is a very powerful abstraction under the concept of lists, and a great example of what I'm arguing.