Which Programming Paradigm Gives the Most Expressive Code?
Warning: this post gets into a very opinionated subject. You may agree with some points, you may disagree with others, it may trigger controversy, and you may be tempted to seize your keyboard to tell how you have a completely different view on programming.
This is exactly what I want you to do. Rather than pouring down my opinions, my point is to ignite a debate with you on how to use programming paradigms to write expressive code.
In fact, as you’ll see, I will largely quote the opinions of some other people I had the chance to watch or talk to. There is no right or wrong answer so please, let us hear your opinion too! End of warning
In her closing keynote of Polyconf 2017, Anjana Vakil classifies programming paradigms into 4:
- the imperative paradigm: the program consists in a structured set of instructions for the computer to execute,
- the object-oriented paradigm: the program consists in objects that send messages to each other,
- the functional paradigm: the program consists in functions that call each other,
- the declarative paradigm: the program consists in statements that describe the intention of the programmer.
They roughly correspond to overlapping periods in the history of computer programming.
Some languages fall into one of those paradigms. For instance, Haskell is totally in the functional programming paradigm, whereas SmallTalk is in the object-oriented one. Some other languages, like C++ or Python, span across several programming paradigms.
So choosing a programming paradigm doesn’t always mean choosing a language. It can also be programming in your language with a certain programming style. As Steve McConnell puts it, rather than programming in a language, you want to program into a language. This essentially means that we should strive to work around the capabilities of a language to implement what we think is the most adapted style possible.
So, what is the best paradigm to write expressive code in C++, or in any other language?
Functional is the new object-oriented
It’s funny to see how paradigms shape our view on programming.
Well, that’s essentially the definition of a paradigm after all, but it’s striking to dig writings from the past and see how the world of programming sometimes revolved around a unique concept.
Take for example the excellent article Arguments and Results from James Noble. This article was published in 1997 and, back then, the ultimate view on programming was to be object-oriented. The hype was then around Java, the OO part of C++, SmallTalk and the like.
In this article, James describes object methods as “protocols” and he writes:
a program’s protocols are the glue that binds its objects together
He also states that “Object’s protocols also known as interfaces are very important in object oriented design”, but the above quotation says a lot about what the emphasis was on at the time: objects.
The 90’s were the glory of the object-oriented paradigm, but today this is no longer quite true. Objects still have a large share in programs, but today the functional programming paradigm is becoming increasingly popular.
We’ve seen functional languages, such as Haskell, that had lived in the academic field for decades, burst out to the public attention over the past few years. So much so that some universities that I know (EPITECH in Paris) require their students to hand in some of the assignments in Haskell.
Also, the functional trend is seeping its way into mainstream languages, with Java 8 taking on streams and lambdas, and modern C++ also including lambdas and (hopefully, hopefully soon) range-like constructs.
What’s so good about functional programming?
One typical property of functional programming is that it forbids mutable state. In pure functional programming, variables don’t even exist. And in imperative and object-oriented programming, a lot of bugs come from a mutable state… that is not in the state it should be. This immutability property allows to get rid of a whole class of bugs, and is at least partly responsible for the success of functional programming.
Also, not having mutable state has another nice consequence: when reading code, you don’t have to maintain in your head the various states of the program. This makes the code that much easier to understand, and that much expressive.
Is object-oriented hopeless then?
Seduced by the modern trend of functional programming and its advantages, one may be tempted to think that the object-oriented paradigm is a steam engine of programming: it was convenient at the time we didn’t know better, but its most useful place now is in a museum.
But programming paradigms aren’t really like trains.
Rather, when a programming paradigm ousts another one there are two things we can do:
- find what the two paradigms have in common. This points us to even deeper programming concepts that outlive paradigms,
- search for useful ways to make them work together. After all, all paradigms had enough traction to change the world’s view on programming. There must be some of their specific aspects that are worth saving from the trash, right?
Let’s dig into those two aspects.
Modularity and its siblings don’t want to die
One of the big things about objects is that they expose an interface, that hides data and behaviour that stay confined to an implementation behind that interface. Then the other objects can evolve around that interfaces, without seeing the implementations.
This concept of exposing an interface has several facets:
- modularity and decoupling: interfaces create clear boundaries between the various objects interacting together and helps making them independent from one another. This makes the system more structured, and therefore easier to understand and change.
- encapsulation: the implementation being hidden from the clients of an object, changing it only goes so far as the internal boundary of that objects delimited by the interface.
- polymorphism: another possible change is to swap out an implementation of an interface for another. In object-oriented systems, this can even happen at runtime with
virtual
methods,
- levels of abstraction: an interface defines a level of abstraction, and its implementation is at the level immediately below it. And writing expressive code mostly comes down to respecting levels of abstraction.
It turns out that those concepts are also present in functional programming. For one thing, functions define an interface and you could nearly replace the word “object” with “function” in the 4 above concepts. But functional languages also have more elaborate ways to define interfaces, like with Haskell’s type classes.
And when you think about it, those concepts where also present in imperative programming, although in a much more basic way: by splitting the code into subroutines.
All this illustrates the interesting fact that modularity, encapsulation, polymorphism and levels of abstractions are fundamental notions to write good code, and they transcend programming languages and even programming paradigms.
This ties up well with the two main aspects of programming defined by Steve McConnell in Code Complete: managing complexity and reacting to change.
Making paradigms collaborate
Another way to benefit from the variety of paradigms is to make them work together.
John Carmack takes the position of avoiding mutating state in general, whenever this is practical and doesn’t kill performance. In the rarer cases where it does impact performance, he advocates to be practical and go for the side effects (for instance don’t return a copy of a C++ standard container each time you want to add something to it).
I have had the opportunity to talk with Michael Feathers about that question too. His view on the topic sounded quite sensible to me: a good way to mix object-oriented code with functional code is to structure your code with objects, and implement their methods in a functional programming style.
It seems to me that there are also cases where using objects inside of a functional-like piece of code can help making the code more expressive.
Take the example of curried objects. These objects can lump several functions together and make them share a common input. They can even play the role of adapters by embedding some logic and translate an interface into another.
Actually, and I know this can sound surprising in this context, those objects can even hold some mutable state. Take the example of an object that builds a CSV line from various strings that you send to it. The object remembers if it is at the beginning of the line to know whether or not to write a comma. If you have to have a mutable state, you might as well encapsulate it in an object rather that let it spill over the various call sites in the code.
The declarative paradigm, the ultimate style or just another passing paradigm?
The declarative paradigm is a interesting trend even though there aren’t as many programs that use it compared to the other paradigms. By declarative, I mean code where you merely state your intentions of the desired behaviour, as opposed to how to implement the desired behaviour.
At this stage, this style seems very attractive to me. Indeed, you have to state your intentions at some point, as the computer can’t figure them out for you. But if we could do only that, it would transform the face of programming.
When I talk about expressive code, I usually describe this as code where you can understand the intent of the person who wrote it. If the code were merely intentions, well, we would be there.
However, it seems difficult to achieve from a practical point of view today. The closest we can get is by designing declarative interfaces, and writing code to implement them.
One example is by implementing Domain Specific Languages (DSL). Another example we’ve come across is to use a rules engine to make declarative if statements:
auto isAGoodCustomer = RulesEngine{}; isGoodCustomer.If(customer.purchasedGoodsValue()) >= 1000); isGoodCustomer.If(!customer.hasReturnedItems())); isGoodCustomer.If(std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders)); auto isNotAGoodCustomer = isAGoodCustomer.Not; isNotAGoodCustomer.If(customer.hasDefaulted()); if (isAGoodCustomer()) { // ... } else { // ... }
Apart for a few technical artifacts, this syntax of this code shares similarities with a specification document that expresses the intent we have for the program.
Declarative programming is a fascinating area, and if you have ideas on how to write declarative code, I would be grateful if you shared them with me.
Let’s discuss programming paradigms
If you found this article interesting and generally agree with it, that’s cool. But if you disagree and have a different view on programming and paradigms, it’s great, and now is the time to let your voice be heard in the comments section that’s all yours!
Related articles:
- Using a rules engine to make declarative if statements
- It all comes down to respecting levels of abstraction
Share this post!