Declarative programming: A complete users guide
Discover “What is declarative programming?” Plus, learn about the value it delivers in relation to business value.
If you’re familiar with application development, you’ve probably heard about declarative programming languages or declarative infrastructure before. To many these are just buzzwords, but declarative programming concepts are quite powerful.
In this post, we’ll explore:
- What is declarative programming?
- Declarative programming vs. imperative programming
- Advantages of declarative programming
- Disadvantages of declarative programming
- Examples of declarative programming
- Declarative infrastructure
- How declarative infrastructure simplifies container management
What is declarative programming?
Declarative Programming aims to describe your desired result without (directly) dictating how to get it.
If you’ve researched this topic before, you may have come upon simple examples like:
order = sorted(filter(fruit.all(), [is_ripe, on_sale]), by=price)
Followed by a generic banality: “it’s just that easy!” Yes, a declarative style can sometimes promote more readable code, but those details have to come from somewhere—often they are abstracted into reusable components. However, declarative is not synonymous with intuitive, and the implications of a declarative design can affect much more than just readability.
total = add(42, 19);
print(total); // output: 61
If the operation “add” is truly referentially transparent, we can replace the first statement with:
total = 61;
Formally speaking, this is the only requirement of declarative programming. Other properties are either derived from this, or shared with imperative programming, making referential transparency the primary difference between the two paradigms.
An aside about side effects
A commonly cited aspect of declarative code is that it does not “produce side effects,” formally described as “referential transparency.” Referentially transparent code relies only on the input to a procedure when determining the output. For a program to have this property, you should be able to replace any expression with its result. As an example of referential transparency, consider a function that adds together two numbers given as input and then returns their sum:
total = add(42, 19);
print(total); // output: 61
If the operation “add” is truly referentially transparent, we can replace the first statement with:
total = 61;
Formally speaking, this is the only requirement of declarative programming. Other properties are either derived from this, or shared with imperative programming, making referential transparency the primary difference between the two paradigms.
Declarative programming vs. imperative programming
The distinction between “declarative” and “imperative” may seem superficial or pedantic, but using the appropriate techniques for the situation will have tangible benefits. Where declarative programming favors a description of the target state, imperative programming details the actions that should be executed in order to produce that outcome. Let’s examine that distinction in more detail.
Often declarative programming is described as simply being “not imperative,” which is not a particularly precise or helpful definition. Beyond that, many explanations will suggest that declarative programming describes “what,” while imperative programming describes “how.” Again, this can be intuitively true, but it doesn’t give us much new information if a reader doesn’t already know what this means. Much like these definitions, the declarative programming style relies on some prior knowledge held by the program, the programmer, or both. Returning to our fruit example above, the “filter” and “sorted” operations are assumed to be defined elsewhere, meaning they must, fundamentally, be imperatively implemented, at least at some level of abstraction.
To try to portray the distinction more concretely, let’s consider a piece of music written for the piano. The traditional sheet music could represent “declarative code” in this case: the melody is encoded and conveyed to the musician using prior knowledge of musical notation. A hypothetical imperative version might instead dictate the starting position of the musician’s hands on the keys, then give instructions on which fingers to press, how and when to shift hand placement up and down the keys.
Advantages of declarative programming
“Stateful” code relies on some data (state) beyond the input to the procedure when determining the result. Think of the starting position of the musician’s hands in the imperative piano example. As a side effect of running, it may modify that state--meaning that subsequent executions could produce different behavior. As an example of stateful code, consider a function that returns the next unique ID for a new database entry incrementing a counter each time it is invoked. Declarative programming techniques eschew stateful interactions wherever possible, minimizing the factors that could affect the behavior of a piece of code; only the input to a function should affect the output, not what’s happening elsewhere in the program (or happened in the past).
Declarative programming favors immutability whenever possible, meaning the state of an object can’t be modified. Immutable objects are easier to reason about. Once the initial state is established, it does not change—it is not subject to side effects. Sharing immutable objects between threads eliminates the chance of data being changed “out from under” a procedure, i.e. a race condition. In addition, this guarantee can provide substantial performance benefits.
Disadvantages of declarative programming
By avoiding dependencies on external state, declarative code is inherently more self-contained. Behavior of each section of the program is only driven by direct inputs; so as long as the “building blocks” of a functional pipeline are understood, an engineer can follow localized procedures without requiring total mastery of the broader program context. This property allows programs to more easily be reduced into intuitive, isolated sections, which in turn are easier to maintain and easier to test.
This abstraction may sometimes hide implementation details that could be important to understand in context. Perhaps the simple and intuitive approach comes at a performance cost—the structure and computational complexity of an algorithm could be entirely obscured by the programmer.
Declarative programming has a reputation for its use in academic settings, focused on theory rather than pragmatic solutions (a stigma that can discourage newcomers). While it is true that functional and declarative programming are the frequent subjects of higher education research, there is plenty to be gained from understanding its practical application. As with any paradigm, it's important to remember that declarative programming is simply a different lens for viewing a problem—one that can sometimes lead to a solution that is easier to conceptualize and maintain.
Declarative programming languages
A declarative programming language prioritizes the declarative style over imperative techniques, either by utilizing syntax and language features to make the preferred style natural, or in some cases even enforcing the preference by rejecting imperative code. “Pure functional” languages forbid the use of imperative programming procedures that produce side effects and manipulate external state.
Example declarative programming languages list
- Prolog
- The Lisp family of languages (Common Lisp, Scheme, Clojure, etc.)
- Haskell
- Miranda
- Erlang
This list is far from exhaustive, and many developers are already taking advantage of declarative concepts and techniques outside of these specific declarative programming languages. SQL is one prominent example that most application developers have worked with. The structure of a SQL query is inherently declarative, providing a description of the resulting data. For example: Produce all entries WHERE my condition is met. Rather than dictating the actual implementation of a query (which will vary substantially between databases), SQL is used to describe the result.
The popular "infrastructure as code" paradigm emphasizes using a declarative configuration of resources to describe the target state of your environment. Widely used tools like Terraform and AWS CloudFormation take this configuration as input (rather than a list of steps to run) and are responsible for reconciling the desired and actual state.
Infrastructure as (declarative) code
Declarative infrastructure takes the concepts that we just examined for writing software and applies them to infrastructure, or building an environment for the software to run in. We can go back to stateful vs. stateless here. If we are relying on the state of a system (a server, cluster, database, etc) and something fails, we lose the integrity of that state and are likely in trouble. However, if we describe our system in a stateless way, we don't truly lose anything if something breaks. Instead, we’re able to recreate it according to the predefined "plan".
Orchestration tools like the open source Kubernetes project seek to apply declarative patterns to containerized workloads, as well as the surrounding infrastructure. This model is core to the way Kubernetes functions—a user communicates their desired infrastructure to the API while a series of “controllers” handle the reconciliation of current vs. target state.
With declarative infrastructure, we can let the application specify what resources it needs while the orchestration layer handles how those resources are provisioned. This way, the application becomes less coupled with the specific minutiae of the environment, which means developers can focus on business logic.
Containers and declarative programming
Containers have seen a huge surge in popularity over the past decade, partially due to the success of container orchestration tools like Kubernetes. But container management solutions can be challenging to implement and manage at enterprise scale. In fact, when it comes to cloud container usage and adoption, 65% of tech leaders report they will turn to 3rd party platforms for container management. The same core concepts used by declarative programming help power these solutions.
Simply put, declarative infrastructure lets us describe the “what” when provisioning a trusted container orchestration platform. These systems allow an organization to define its needs without specifying how they should be achieved, delegating opinionated decisions to the platform itself, which continuously drives toward the pre-defined target state.
Navigating containerization adoption challenges
As organizations quickly move to containerize applications and adopt container management tools, they may find a few things to be true:
- There’s a talent shortage: Many teams need to quickly hire developers with expertise in Kubernetes, which are in high demand and short supply. A May 2019 Forbes article estimates that over 5 million IT jobs will be added globally by 2027. According to research from talent search platform DICE, Kubernetes is within the top 2 requested skills of those 5 million open roles.
- Environment setup becomes much simpler: Containers can be deployed from a single source controlled file, containing environment, configuration, and access specifications for your application. Applications composed of multiple microservices, each using an individualized image from a registry, can easily be designed and deployed.
- Your organization’s resources are already overprovisioned. Regardless of which cloud environment a team may choose, resources such as CPUs, hardware, storage, and IP addresses are generally costly and in high demand. It quickly becomes difficult to manage scaling up and down based on demand as more applications run on the same infrastructure.
Implementing a single orchestration solution can provide tremendous lift in those three areas.
Look for one that can codify the following:
- Container setup and configuration management
- Infrastructure provisioning
- Load-balancing
Rather than spending time training current developers in all of these foundational areas, relying on declarative programming and a reliable container tool to take care of the basics helps them get back to their job: shipping excellent code to meet business objectives.
Container tools built by Capital One
At Capital One, we’ve built our own container tool that runs on top of Kubernetes with declarative infrastructure in mind. Critical Stack is a simple, secure container orchestration platform built to balance what developers want with the needs of our organization. By combining improved governance and application security with easier orchestration and an intuitive UI, we’re able to manage containers at scale safely and effectively.