The primary difficulty with object-oriented programming should be readily apparent: our task takes a
lot of code in this paradigm, and there are a lot of interoperating parts. In the imperative code, we lost the
forest in the trees: implementation details became confusing and interfered with seeing the big picture.
Object-oriented programming intends to solve this problem by cleaning up the trees. The implementation
details are a lot cleaner, but it is still easy to get lost. It is a common experience for a new developer to enter
onto an object-oriented project and be at a loss for how the system works, especially if the system does not
have an obvious entry point (such as a main method). The problem is that it is hard to follow the flow of
the application: it is hard to understand how these parts all interact during the execution of the system. For
instance, the word loader takes a line id, but if you were new to the system, it could easily be unclear what
that line id was or how it was calculated.
This is not to say that object-oriented programming is bad. If you need a highly modular, highly
decoupled system, it is the perfect paradigm. Tools like interfaces and abstract base classes allow you to
provide clear contracts while still being nice to your consumers. It is also an excellent approach if you need
a highly testable system: it is not a coincidence that practices such as test-driven development arose out
of object-oriented languages. The testability advantage comes from the fact that functionality is entirely
contained within a single object, so it is possible to treat that object as its own independent microcosm
of reality. The object-oriented model also fits with concurrent execution much better than imperative or
some other paradigms, since the clearer separation of concerns make for clear breakpoints. Borrowing key
practices from functional programming makes object-oriented concurrency even nicer: it was this discovery
that created the post-functional paradigm.
Post-Functional Programming Paradigm
Object-oriented programming was heralded as a major revolution in programming, and it did do a lot
to improve the maintainability and reusability of large codebases. However, mutability became a major
problem in object-oriented programming. Another common problem came from mixing data and flow:
underlying the object-oriented paradigm is still the imperative paradigm, but the encapsulation of data
also led to obscuring the logic that acted on that data. Both of these problems are addressed by changing
the underlying model of execution from imperative to functional: that is, instead of thinking of executing a
series of steps, you think about applying transformations and mappings. This gave rise to the post-functional
Post-functional programming makes it much easier to reason about your object-oriented application.
By strongly limiting mutability and focusing on composing small transformations, we can minimize the
drawbacks in object-oriented programming while still leveraging its advantages. Technically, the post-
functional paradigm gets away from the imperative paradigm, so the programmer worries about what to
implement and not how it is executed. This results in more semantically meaningful code, but also frees the
runtime to decide the best way to execute the transformations. The result is improved performance for little
to no cost, especially when executing in a concurrent environment.
If you have read this topic, then you should hopefully be able to spot post-functional code in Java. The
ubiquitous use of streams and lambdas is the obvious sign. The use of the final keyword is another major hint,
especially if that keyword is occurring in a large number of very small classes, each of which does a very limited
and very precise transformation on data. Before Java 8, the key sign for post-functional Java code was collection
comprehensions: methods that took in a collection and performed some user-specified operation on it.
To implement our post-functional solution, we will start by constructing our schema by executing each
string in a stream of DDL statements. This will get our database into the proper format. We will then get the
anthology text as a stream, and filter out the undesirable elements from that stream. Now we have a clean
stream and a database initialized, so our goal will be to produce a stream of data to insert into the database.
The intermediate form will be a stream of lines coming from the input. So we will go from a stream of Strings
to a stream of parsed lines to a stream of words to insert into the database. That final form can then be
inserted into the database. The code for this is given in Listing A-4.