Declarative programming and magic — Part I
In Concepts, Techniques, and Models of Computer Programming, Roy and Haridi define declarative programming thus:
We say the operation is declarative if, whenever called with the same arguments, it returns the same results independent of any other computation state. A declarative operation is independent (does not depend on any execution state outside of itself), stateless (has no internal execution state that is remembered between calls), and deterministic (always gives the same results when given the same arguments).
This is but the very definition of pure-functional programming as it is commonly understood today. So in a section titled “What is declarativeness?” they go on to clarify thus:
We have defined declarativeness in one particular way, so that reasoning about programs is simplified. But this is not the only way to make precise what declarative programming is. Intuitively, it is programming by defining the what (the results we want to achieve) without explaining the how (the algorithms, etc., needed to achieve the results). This vague intuition covers many different ideas, and can be classified based on expressiveness. There are two possibilities:
One possibility they discuss is programmable declarativeness. It essentially models a pure functional language: zero mutable state and no I/O. This is kind of mathematical — a language to define abstract truths, compose them to form larger but consistent wholes, and run computations using them. This language subset however never interacts with the tangible world.
This pure-functional meaning is used throughout the book when discussing declarative programming, except in the chapter on GUIs where they use the word to mean what they call descriptive declarativeness:
Descriptive declarativeness. This is the least expressive. The declarative “program” just defines a data structure. This language can only define records! For example, defining graphical user interfaces. Other examples are a formatting language like HTML (Hypertext Markup Language), which gives the structure of a document without telling how to do the formatting, or an information exchange language like XML (Extensible Markup Language), which is used to exchange information in an open format that is easily readable by all.
The descriptive level is too weak to write general programs. So why is it interesting? Because it consists of data structures that are easy to calculate with. HTML and XML documents, and declarative user interfaces can all be created and transformed easily by a program.
That is the prominent shade through which I see declarative programming — literally declaring things. We already have the phrase pure-functional to talk about idempotent code, but what other word best describes this beautiful piece of ActiveRecord?
validates :registration_code,
length: { is: 8 },
allow_blank: true,
numericality: { only_integer: true }
For the rest of this series, I use declarative programming to mean this — declaring truths about complex systems, not just abstract ones, but stuff that can talk to databases, fly through networks, make games with, and do all sorts of messy real-world stuff.
Declarative programming is considered a powerful lever in software construction. We define systems by stating facts about it and it magically whirs into existence. Thus it is looked upon to be the pinnacle of the craft, the ultimate abstraction being a domain specific language.
But the declarative language itself is extremely limited in power. While imperative languages are turing complete, the only thing a declarative language can do, Roy and Haridi says, is declare facts and stuff them into a list. ActiveRecord is a good example — all the validation rules we specified were simply added to a list of rules; and the actual execution of these rules happen out of band.
This list of rules is simply data, not executable code. They’re meaningless bytes with no agency, and becomes code only when fed into a machine that can understand and execute it. This machine is where the magic of declarative programming begins, both in its awe-inspiring sense, as well as in its oft-maligned cognitively expensive sense. Here is a Dijkstra gold that shines light on this cognitive cost:
Our intellectual powers are geared to master static relations and our powers to visualize processes evolving in time are relatively poorly developed. For that reason we should do (as wise programmers aware of our limitations) our utmost to shorten the conceptual gap between the static program and the dynamic process, to make the correspondence between the program (spread out in text space) and the process (spread out in time) as trivial as possible.
The conceptual gap between the static program and the dynamic process has a name. It is called action at a distance.
Action at a distance
Here is how Wikipedia defines it:
In computer science, action at a distance is an anti-pattern (a recognized common error) in which behavior in one part of a program varies wildly based on difficult or impossible to identify operations in another part of the program.
It is a broad term and can describe many things — asynchronicity, callbacks, concurrency, and even simple global state mutation:
var i = 0;
function increment() {
i = i + 1;
}
increment();
Here increment() does a kind of action-at-a-distance: global state coupled with free-standing functions that mutate them.
Rob Pike, a master of the craft and author of GoLang, has a vocal dislike of everything happening at a distance, and Go manages to avoid it more than any other language out there. Python has “explicit is better than implicit” in its Zen, but only GoLang does it full justice. In Go every error that could ever happen is explicit and there are no exceptions to magically pop up from underneath the visible, tangible code. The static text is the only source of truth during runtime — you can always trace the program-counter back to its precise line of imperative code.
I hope you’ve observed a tug-of-war here. On one hand we’re to exploit the power of declarative programming through magical abstractions याने virtual machines. And on the other, we’re to do our utmost to shorten the conceptual gap between the static program and the dynamic process.
There is of course a balance to be found, and in the next post I’ll explore where it may lie by digging deeper into declarative magic.