Abstraction is one of the greatest visionary tools ever invented by human beings to imagine, decipher, and depict the world. – Jerry Saltz
I wish to approach truth as closely as is possible, and therefore I abstract everything until I arrive at the fundamental quality of objects. ― Piet Mondrian
One of the most advocated "best practices" in programming is the DRY principle: Don't Repeat Yourself. Many techniques can be used to enforce this principle: encapsulation, parameterization, inversion of control, and many more. One of these techniques is abstraction and one main difference between functional programming (FP) and object-oriented programming (OOP) is the way abstraction is applied. A common practice in OOP is to limit abstraction to the strict useful minimum for the problem at hand. In OOP, premature abstraction is often considered a fault, much as premature optimization.
In FP, on the other hand, abstraction is generally pushed as far as possible. Every problem is broken into a series of the simplest possible functions, which are then composed to build the problem solution. Identifying these abstractions is generally the most important part of problem resolution. In fact, FP programmers often spend more time trying to find what problem they should solve than solving them. And of course, it generally appears that these functions are the same from one problem to the next. Only the way they are composed is different. This is the reason why abstraction is one of the most valued techniques used by FP programmers.
In this article, we will compare how OOP and FP would handle abstraction in a specific simple problem: computing the sum of integers between 1 and an arbitrary value n. The problem is so simple to solve using imperative programming that it seems there is nothing interesting to learn from it. Here is how it could be done in imperative Java:
static int sum(int n) {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
It seems that there is no simpler and more efficient way to do it. However, for an OOP programmer as well as for an FP programmer, the interesting part of the problem is still to come. But each of these programmer will probably go a very different route. We could also notice (and demonstrate!) that the sum of the n first integers is equals to (n * (n + 1) / 2)
. As you will see, this would be going the other way, towards the least possible abstraction.
Abstracting the Computation in Object Oriented Programming
Both FP and OOP programmers will immediately remark one possible abstraction, which is the operation that is applied to construct the result. Here, we are adding values to construct the result. What if we were asked to return the product instead of the sum?
In OOP, there are two obvious way to abstract the computation: using the strategy pattern, or inversion of control (IoC).
Using the Strategy Pattern
The Java way to abstract a computation in a form that can be handed to a program is to create an object containing the methods performing the computation. (If you're lucky and there is just one method, lambda expressions can make this simpler but the approach remains the same.) For this to be an abstraction, we have to make the abstract part into an interface and the concrete part into an implementation:
static int compute(int n, Computer computer) {
int result = computer.identity();
for (int i = 1; i <= n; i++) {
result = computer.compute(result, i);
}
return result;
}
interface Computer {
int compute(int a, int b);
int identity();
}
static class Adder implements Computer {
@Override
public int compute(int a, int b) {
return a + b;
}
@Override
public int identity() {
return 0;
}
}
static class Multiplier implements Computer {
@Override
public int compute(int a, int b) {
return a * b;
}
@Override
public int identity() {
return 1;
}
}
public static void main(String... args) {
System.out.println(compute(5, new Adder()));
System.out.println(compute(5, new Multiplier()));
}
Here, we have abstracted the computation and are now able to reuse the program with a different computation strategy, such as the multiplication. Note that we put the identity
element, used as the start value for the computation, into the various Computer
implementations, since it will be different for different types of computations (0
for addition, 1
for multiplication).
Frankly, it does not seem like a great improvement, and many programmers would not see the interest of decoupling the computation from the iteration. Furthermore, this approach does not take into account that we have in fact two nested possible abstractions: The operation itself (addition or multiplication) and the identity concept of a specific value that is paired with an operation and does not change the value to witch it is associated through this operation. The value 0
is the identity for addition, and 1
is the identity for multiplication. Many other operations have an identity value. The empty string is the identity for string concatenation, and the empty list is the identity for list concatenation.
We could of course use the Strategy pattern again for the identity, but this would create a really messy solution, and most programmers would probably think that using an interface with two methods is enough abstraction. As you will later see, FP programmers do not think this way.
Using Inversion of Control (IoC)
Another more object oriented technique for abstracting the computation is called inversion of control. The strategy pattern solution uses composition to solve the problem whereas inversion of control is based upon inheritance.
With this technique, we construct an abstract class that performs the loop, calling an abstract method to get the start value and another one to do the computation:
static abstract class Computer {
public final int compute(int n) {
int result = identity();
for (int i = 1; i <= n; i++) {
result = doCompute(result, i);
}
return result;
}
abstract int doCompute(int a, int b);
abstract int identity();
}
static class Adder extends Computer {
@Override
public int doCompute(int a, int b) {
return a + b;
}
@Override
int identity() {
return 0;
}
}
static class Multiplier extends Computer {
@Override
public int doCompute(int a, int b) {
return a * b;
}
@Override
int identity() {
return 1;
}
}
public static void main(String... args) {
Computer adder = new Adder();
System.out.println(adder.compute(5));
Computer multiplier = new Multiplier();
System.out.println(multiplier.compute(5));
}
This approach is probably the most object oriented one, and it might seem much cleaner than the strategy pattern because it seemingly also abstracts the iteration into the Computer
class. (By the way, this specific use of IoC has been given a specific name: the template method pattern.) But in fact, we did not abstract the iteration! We just encapsulated the whole program in the computer class. Let's now see how a functional programmer would handle the problem.
Abstracting in Functional Programming
Continue reading %Lessons in Abstraction: What FP Can Teach OOP%
by Pierre-Yves Saumont via SitePoint