DEV Community

Oleg Gromov
Oleg Gromov

Posted on • Originally published at oleggromov.com

Declarativity Comes at a Price

There is a widely held notion, and I agree, that declarative code is better than imperative. It is less error-prone, usually much more eloquent and neat and thus much more maintainable. It is a good principle to follow on a day-to-day basis when you use existing declarative libraries like the JavaScript standard library (Array/Object methods etc.), underscore/lodash or React. However, when it comes to making a decision to either write some declarative code (and therefore much more generalized) or just leave an imperative ad hoc solution, I suggest thinking at least twice.

Every algorithm has specific complexity; not only does it have computational complexity (declared by means of time/memory it takes to run on different input sizes), but also a complexity of writing and understanding the code that implements it. The latter primarily falls to programmers who work with the code, and in the world of budgets and deadlines is also a valid point of concern.

What I state in this article’s title, declarativity comes at a price, can be split in a few theses.

Implementation Cost Might Be Higher

Whenever you decide to write something declaratively and have already implemented required primitives, like map or filter for arrays, you might be fine. Someone has already spent time writing and debugging it, and the only thing is left to do is understand how these primitives work and build your own algorithm of these small pieces. What is even better, once you understand how these building blocks work you acquire a rare skill of writing concise code with ease, which must be appreciated by virtually any good programmer in the world.

On the other hand, when a problem you’re solving or a solution you’re picking is not so common and you cannot find needed “blocks”, you might be facing a tough decision of implementing them on your own. Sometimes generalization and abstraction require much more effort you will or can make.

Comprehension and Debugging Price Might Be Higher

Declarative code looks clearer at first glance but this impression is likely to change when it comes to deep comprehension and debugging. Simple things like map or filter are relatively well known and understood, more complex things like React’s component lifecycle are much more sophisticated, and self-implemented and likely not documented and/or tested primitives can be your worst nightmare. You might end up unraveling abstractions your coworker came up with a long time ago or even rewriting them in order to make it work as you expect.

Generalized solutions of abstract problems, which declarative code comprises in essence, are hard. When you generalize an idea and explain it to your friend you might simplify it as well for sake of understanding; when you generalize algorithm, on the other hand, you likely create more and more edge cases because otherwise the code won’t work. It leads to more complications compared to an original smaller issue, more tests to be written and more time to be spent comprehending.

And furthermore, we love eloquence and neatness in our code. A generalized solution might be not so good in the first place and you’ll spend hours to make it look better, whereas the original problem’s solution could have taken only a few minutes.

Conclusion

Don’t get me wrong, I agitate neither against declarative code nor for it. Just be aware of the costs. When you’re lucky to have necessary tools, go ahead and use them. When you encounter a stinky imperative code in your shiny declarative React app, you most likely have a reason to be concerned. Just try to think twice and make a deliberate decision. We won’t get closer to good and working code just echoing dogmas that are being said on every corner. Considering implementation and comprehension and debugging costs, on the other hand, might help to come up with the right decision, whether it is leaving the code as is or paying the price for making it declarative and more generalized.


This article was originally posted in my blog oleggromov.com

Top comments (10)

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

I think there are a few branches to this topic of "declarative code" and they should not be lumped together.

The shiniest example given is that of map and filter. Operations like these bring a large value with minimum cost to understand and maintain. These operations transcend language and platform, being rooted generally in math. So you can find them in a lot of different languages and at different levels. E.g. SQL SELECT is map, WHERE is filter, etc.

Another possible interpretation is using a framework with conventions, so you can declare things instead of writing procedural code. Some examples might be ORMs where you use an attribute to declare a property as a primary key. Or a Dependency Injection framework that automagically wires up interfaces with an implementation at run time. These start heading down the path of requiring specialized and/or non-portable knowledge. Advanced usage sometimes ends up being as much or more work as doing things manually.

Yet another variation I can think of is the tendency as developers to over-abstract. This is easiest (but by no means limited to) early in a programmer's career. We play the "what if" game, and program for a number of scenarios that never actually happen. This can extend to implementing an entire framework around our code, ostensibly to make it easier to add new use cases. But the custom framework just gets in the way of those use cases you couldn't predict. It becomes a case of designing the plumbing first and building a house around it.

So I would rank how desirable it is to be "declarative" differently between these cases. Common declarative operations like map and filter, it is hard to see any significant downside to using them. Published "frameworks", although there are probably some good ones, I tend to avoid these in favor of a "library" of operations (e.g. Dapper instead of EntityFramework). For plumbing code that I write, I usually find it best to avoid prescriptive abstractions. Instead I try to keep things course grained. When several use cases have some logic overlap, I'll develop a "helper" function for that combination of operations. That way new use cases can choose to call a helper if it fits, but can fall back to managing everything itself if needed.

Collapse
 
oleggromov profile image
Oleg Gromov

Kasey that's a perfect complementary comment, thanks!
Hope you don't mind if I use your points to make my reasoning more concise and useful.

The thing that inspired the article is a mix of your first example related to "advanced usage" of a declarative tool with a temptation to over-abstract. In my project, I had different consequent and/or parallel database requests to be made on different API calls. After writing the 3rd function performing db requests in another sequence but still very similar manner, I realized that it might be a good chance to step ahead of current problems and solve it on a higher level.

I decided to make all possible combinations of subsequent and parallel requests configurable with support for result passing between them. The process was complicated with the fact that at the moment I tried to use promises and callbacks together producing horrible code style I couldn't understand.

So I tried to make a barely readable config of requests, queries to perform, their parameters and error messages; a bit cryptic code to perform required function calls against the config; and deleting code I didn't like in the first place. The latter in fact was bad but not because of the imperativity but because of the weird mix of Promises, which is a javascript abstract object incapsulating an async action behind the interface to handle its results, and callbacks, which was a usual way of writing asynchronous code without any libraries a while ago.

The "declarative" solution I ended up with was even worse than the initial code I tried to make prettier and better. Here's where my point about writing your own declarative libraries/frameworks/whatever takes effect. It is hard because understanding abstraction takes time and effort to make, and so does the actual coding of the declarative tool.

Did I try to solve an existing problem? Yes, I did, but the problem lied not in a plain of declarativity/imperativity.

Could declarative code help? It might have helped if I used an existing query-chaining library. Writing my own library would take so much time - and the code I wrote in a few hours hadn't really solved a problem, not even awkwardly.

Did I try to over-abstract hiding requests complexity in a config? I definitely did.

Finally, by getting rid of callbacks and leaving only Promises, I managed to make the code very readable and clean. There are rather complicated use cases like user deletion or list creation, which include simultaneous DELETE or INSERT affecting a few tables, waiting for the results and performing operations on another table, or simple ones like list retrieval.

Collapse
 
kephas profile image
Nowhere Man

What I get from this is that you tried do transform code that was problematically promise-heavy to a more declarative form and that form didn't solve anything.

Am I missing something?

Because the only takeaway I see is that transforming code to a declarative form isn't magical in that it would solve any deficiency of the underlying code.

I don't see how that supports the notion that declarative code might be too costly.

Thread Thread
 
oleggromov profile image
Oleg Gromov • Edited

Declarative code itself might be not pricey at all - it's clean and nice, no problems.

My example shows that sometimes transformation to declarativity might cost a lot - because it is hard to make a generalized solution. Could I make a config to perform request in any possible variations? Yes, definitely. Is it worth it? Well, that depends on the use cases, your will and budget to make an abstract solution and whatsoever.

Perform a mental experiment: when you use functional and declarative lodash or underscore it costs you nothing on the surface. But somebody spent substantial time separating, coding and debugging primitives you use. It must sound undoubtedly true. And then imagine your project and you, willing to write declarative nice code but lacking needed libraries. Won't writing a one on your own be way to expensive? It might - that's my point.

Either way, there're perfect examples in the comments related to hidden complexity of the computations or too domain-specific solutions like React, for example.

Thread Thread
 
kephas profile image
Nowhere Man

But transforming to any different form might cost a lot. So again, it says nothing about declarativity and only about code transformation.

Transforming code to CPS might cost a lot. Transforming code to SSA might cost a lot. Transforming code to OOP might cost a lot. etc…

But writing declaratively from the start? Well, then the odds that it would cost anything substantial are significantly lower!

I wouldn't start by writing a whole library, I would just write the declarative functions I need. That's actually how functional programmers work… And it's not hard when you are used to it.

Like structured programming, OOP, concatenative programming, logic programming. Like any style, actually.

Collapse
 
kspeakman profile image
Kasey Speakman

Awesome, glad to contribute to the discussion. :) Thanks for the post!

Collapse
 
t4rzsan profile image
Jakob Christensen

Thank you for your write-up.

I think we can all agree that declarative coding makes your code more readable. C# LINQ is a great example of this.

I think the greatest danger is that you may lose oversight of what the declarative code is actually doing. You may end up iterating your datasets several times without you knowing it.

Just recently I have been mentoring a colleague on C#. After showing him some examples of C# LINQ, he came to me complaining that C# is awfully slow. I looked at his code where he had been using LINQ amply and it turned out he was iterating his dataset (a couple of million of rows) at least 9 times. A bit of changes brought the execution time from 50 minutes to 5 seconds.

Of course, the real problem here was my lack in teaching but it does show that declarative coding makes your code easier to read but less transparent.

Collapse
 
oleggromov profile image
Oleg Gromov

Thanks for your response and a perfect example Jakob!

I'm not familiar with C#/LINQ but as far as I understand it's a tool that hides the complexity of DB/XML searching behind a slick interface. As we can see from your example, there's another point to be considered especially when it comes to junior developers. Declarative tools can unintentionally hide computational complexity - and provide small to no signs of that something goes wrong.

Collapse
 
t4rzsan profile image
Jakob Christensen

Thank you for your reply, Oleg.

It is true that there are LINQ providers for databases and for XML. But LINQ also provides a funtional interface for any type of collections, in the same way as you see reduce, map, and filter in other languages.

Collapse
 
oleggromov profile image
Oleg Gromov

What do you guys think about declarative code implementation and maintenance costs?