Thinking Declaratively in an Imperative World

Functional programming seems to be making its way into everything these days, and you can take advantage of it even if you’re not using a strictly functional language or toolset. The key is to think declaratively rather than imperatively.

Declarative programming is another way to describe functional programming, which emphasizes its opposition to imperative programming. In imperative programming, you’re primarily concerned with the steps that must be done. But when thinking declaratively, you’re more focused on the end result and the transformations needed to get you there.

Here are a few strategies for thinking declaratively, even if you’re stuck in an imperative environment.

1. Consolidate State

“State” is any piece of information in a program that may change while it’s running. It might be the value of a variable or an entire database.

In imperative programming, it’s easy for state to get scattered all over the place. And many times, the same state is stored in multiple places, just in different forms. One problem with storing the same state in many places is the potential for it to get out of sync.

Another thing to consider is the redundancy of state that can be derived from other state. If you can reduce the base amount of state and simply derive the other forms using functions, you’ll have less state to manage overall. And less state is always a good thing.

For example, take this code for managing a shopping order (pseudocode):

class Item {
    title: string
    price: number
    quantity: number
}
 
class Order {
    items: Item[]
    total: number
 
    func updateOrder() {
        var total = 0
        for item in self.items {
            total = total + item.price * item.quantity
        }
        self.total = total
    }
}

The state containing the total for an order is stored in two places: in the Order.total property, but also within the items themselves. In this case, updateOrder() must be called any time the items collection is modified or any item within it changes.

We can consolidate this state by making the total a function, eliminating the need to store the total separately:

class Order {
    items: Item[]
 
    func total() : number {
        var total = 0
        for item in self.items {
            total = total + item.price * item.quantity
        }
        return total
    }
}

2. Start from the End

As opposed to imperative programming, in which steps are laid out one-by-one, declarative programming focuses on definitions. So instead of giving step-by-step instructions to produce output, you define the output in terms of transformations of input.

Going back to the shopping cart example, the function for calculating the order total is currently written imperatively (the for loop is a dead giveaway). Functional transformations are usually constructed from general-purpose higher-order functions like map, filter, and reduce combined with lambda expressions.

The exact names may vary, but many non-functional languages have added support for these constructs (Java has some support by way of streams, and there’s also Guava; C# has had LINQ, which is not quite the same but a good start).

To write this declaratively, we need to figure out how to reduce a collection of items down to a single number, which is the total price. This will require two transformations:

  1. map from an item to its extended price (price times quantity)
  2. reduce the list of extended prices to a single value, by summing
class Order {
    items: Item[]
 
    func total() : number {
        return items
            .map(item =>; item.price * item.quantity)
            .reduce((item1, item2) => item1 + item2)
    }
}

By writing this declaratively, we’ve succinctly described how the total relates to the items. And we’ve eliminated another instance of state (the local total variable).

3. Embrace Immutability

Pure functional languages are built on immutability because if having more state makes debugging harder, then allowing that state to change only multiplies the effect! But of course, state that can’t change isn’t really state at all. So what good is immutability?

Imagine that you have a function that operates on a list of objects. A deep copy of the list would be too expensive, so you operate on the list directly. References to the list or any of its objects in other parts of the program may now be affected.

But if those objects and the list were guaranteed not to change, then you’d be free to create a new list that refers to the old one as a starting point. In fact, this is how persistent data structures work.

Even without language-enforced immutability, conforming to it by convention can eliminate a lot of unexpected state changes.

4. Avoid Side Effects

Avoiding side effects is all about reducing the number of opportunities for state to change. Reproducing an error is usually a process of recreating the conditions that got the program into a certain state. By reducing the number of places where state can change, you can eliminate code paths while debugging.

To do this, strive to write methods that operate on parameters and return value only. This will make them easier to test, and often easier to read as well.