NDepend

Improve your .NET code quality with NDepend

declarative_programming_in_depth

Declarative Programming in Depth

Most people that start programming learn to program in an imperative way. Shortly after, they will probably learn a declarative language too. Many developers go a long way without knowing the difference. Many will probably never know. And they could very well be great developers. But knowing the difference could change the way you write code for the better, so let’s dive in.

What Is Declarative Programming?

This question is easily answered by stating the difference between imperative and declarative programming: in declarative programming, you tell the computer what to do, while in imperative programming, you tell the computer how to do it.

The definition on Wikipedia can also help:

Declarative programming is “a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow.”

What does that mean exactly? Well, in my introduction, I stated that many of us learn imperative programming first, shortly followed by a declarative language. Beginning developers around the world learn languages like C++, Java, VB.NET, C# or Javascript as their first language. These are imperative languages. These beginning developers then soon learn SQL and/or HTML, which are declarative languages.

An Imperative Example

Let’s look at an example in C#:

It should be clear we’re making a total of all orders made by gold customers. It should also be clear how we achieve this:

  • We retrieve the customers. We include their orders to avoid an N+1 problem.
  • Then, we set a goldCustomersOrdersTotal variable to 0.
  • Next, we loop over all customers.
  • If it’s a gold customer, we loop over the orders.
  • For each order, we add the total to our variable.

Let’s look at how this is done in a declarative language.

A Declarative Example

In SQL, the above code sample could be written like this:

In this piece of code, we’re telling the database what we want, not how. Sure, we need to add the join, but we don’t need to know anything about the underlying engine. We don’t have to choose between a scan or a seek, on a table or an index. We don’t need to optimize the query plan or see if it’s already in the cache. We don’t need to tell the server which tables to query first. A mature SQL database has a complex engine to run our simple (and not so simple) SQL queries. We just tell the server what we want and let it choose the best way to get the results.

But Your C# Is Awful!

OK, let’s revisit our C#. In .NET Framework 3.5, LINQ was introduced. This makes it possible to write our code in a much cleaner way:

or:

That’s much better isn’t it? Well, as it turns out, LINQ is a declarative language.

So C# allows for both declarative and imperative programming? Yes, it does. While there are languages that fall into the imperative or declarative category, you can often apply both styles. For example, you can write imperative-style if else code in SQL. And you can write in a declarative way in C# too.

Declarative Programming in C#

We’ll look at declarative programming in C#, but you can easily use the same techniques in your imperative language of choice: Java, JavaScript, Python, etc.

Let’s revisit the idea behind declarative programming: write what you want to do, not how you want it to be done. Of course, at the lowest level, we will have to provide the implementation details of what we want to do. And the flow of our code will still be our responsibility, but the idea behind declarative programming can improve our code.

Simple Methods

One starting point towards declarative programming is to split up long and complex methods into several smaller private methods with readable names. Let’s assume we have an assembly line for a certain product. When the product is finished, our application needs to calculate a score for the overall quality of the product. For example, the quality of the steel produced by a steel mill can be measured by all kinds of parameters. Each parameter could be a set of complex calculations. The exact details are not important for our article. Look at the following piece of code:

In this totally made up example, you can read what is necessary to determine the quality of the product. Regardless of the specific implementation details, this reads more easily than a single long method containing all the details.

But this is just the basics. Let’s dive in deeper.

Extension Methods

Extension methods provide great value to write in a declarative way. Especially because extension methods allow you to add methods as though they were true members of a class that you don’t own.

For example, we once needed to convert a NameValueCollection to an ILookup. We can’t use LINQ’s ToLookup function because it doesn’t support NameValueCollection. And we can’t change NameValueCollection because it’s part of the .NET Framework. But we can easily convert it by writing the following:

As you can see, we’re already using LINQ here. But as far as our own code goes, we’re exposing how we’re converting our NameValueCollection. And what if we need this in multiple locations? Let’s use an extension method:

This allows us to write:

Much more readable, no? But we can go even further.

Higher-Order Functions

Higher-order functions are functions that:

  • either take one or more functions as arguments;
  • or return a function as result.

With higher-order functions, we come close to functional programming, which is out of scope for this article. But I do want to mention them briefly because it extends the power of your declarative programming.

I’ve worked on an application where images needed to be manipulated before being stored to disk. In a simplified form, we had something like this:

To use this code, you would make a call like this:

But what if we change our method to accept a function?

Now we can move the logic to crop or resize the images to separate functions, we lose the need for an enum, and our call is more readable:

In our new ResizeImage method, there are now fewer implementation details. We get the file from the cache, and if there is no file in the cache, we return the result of an image manipulation. The exact details of the image manipulation are not important in this method. This also makes it easier to add new image manipulation methods later, satisfying the open-closed principle: we don’t need to modify any existing code to add new behavior.

Aren’t We Just Hiding the Implementation?

Eventually, every piece of our code must be implemented somewhere. In declarative languages, the implementation is up to whoever interprets the code:

  • SQL is handled by a database engine.
  • A browser will interpret HTML.
  • A YAML configuration file could be handled by a build server.

When we apply some declarative techniques to our imperative languages, it’s up to us to provide the implementation. Even the LINQ extension methods are implemented in C#, they’re just not in our own codebase. So we’re actually just abstracting away implementation details. Then the question is: how far do we go with this abstraction? You will need to find what works best for you, but a good starting point for me is that public methods (i.e. your public API) should contain a minimum set of statements, and these should all be easily readable. The contents of your public method should tell you what is happening, not how.

Some even go as far as to introduce one-line methods. These are methods with only one line of implementation.

In the end, you should decide together with your team. But I hope I have convinced you that writing in a more declarative manner can greatly improve the readability and flexibility of your code.

Contributing Author

Peter is a passionate programmer that helps people and companies improve the quality of their code, especially in legacy codebases. He firmly believes that industry best practices are invaluable when working towards this goal, and his specialties include TDD, DI, and SOLID principles.

Leave a Reply

Your email address will not be published. Required fields are marked *