Ross Esmond

Code, Prose, and Mathematics.

portrait of myself, Ross Esmond
Written — Last Updated

Declarative Programming

Declarative programming is performed with rules that define the relationships between data. These rules are then realized by the underlying declarative system to derive results in a time-independent way. The rules are referred to as the logic of the system, while their realization is referred to as its control. The system uses the rules to determine which events from the programs environment to follow, what data from the environment to observe, how to interpret said rules to derive side-effects, and how to reapply their side-effects in a time-independent way.

The Five Components of Declarative Statements

There are five components of a Declarative Statement: that they be Reactive, Deterministic, Strongly Idempotent, Consistent, and Commutative. Reactivity ensures that the side-effects of the system are always up-to-date. Determinism ensures that the side-effects will never depend on inputs that are not observed for changes. Idempotence ensures that repeated realizations of the rules will not affect their results. Consistency ensures that the rules will never produce an impossible result. And finally, Commutivity ensures that the order in which rules are realized will not affect their collective result.

Reactivity describes the property of a declarative system to observe the input data for a rule for changes in order to reapply the rule whenever they occur. To deliver Reactivity, a declarative system must be able to determine what data the rule injests during its realization, either statically, dynamically, or implicitely. Static analysis is performed by web frameworks like Svelte or SolidJS, whose transpilers can detect the use of input data before runtime. Dynamic analysis is performed by web frameworks like KnockoutJS, which was able to detect the use of inputs whenever they were requested during runtime. Implicit analysis is used by ReactJS, which knows that components may only depend on their own state and properties, and so it may assume that components only rely on those two input sources.

Determinism is one half of purity, the other being a lack of side-effects. In pure functional programming, determinism describes the property of a function to always return an equivilant value given equivilant inputs. Declarative Programming, however, does not require that a rule be pure, only that its effect on the system not depend on time. It is then sufficient for a deterministic rule to apply equivilant side-effects given an equivilant program state, so long as the program state that can alter the rules effects be observed for changes. Any values which the rule injests must either trigger a reapplication of the rule or not alter its outcome.

The meaning of idempotence changes depending on the context. In functional programming it implies that feeding the output of an function back into it will produce the same result:

f(f(x)) = f(x)

In imperative programming it implies that repeated execution of a function with the same arguments will not affect the programs state any further:

mySet.add('value');
mySet.add('value');

In the case of imperative programming you may say that any further calls to the function beyond the first are insignificant, as in the case we have just shown, but you may also say that when you call the function any prior calls made to it are insignificant. In the case of adding 'value' to mySet, if we were to do so in an event handler, we could assume once we added the value that any prior attempt to add that same value are irrelevant, as they no longer have any effect on the program.

Strong Idempotence pushes this idea further, by removing the condition of matching arguments. A strongly idempotent routine will render any prior execution of that routine irrelevant, regardless of which arguments were used:

let number = 0;
function wipeout(arg: number) {
  number = arg;
}

Strong Idempotence ensures that prior realization of the rules will not have lingering side-effects, allowing the declarative system to reapply the rules as necessary without adverse results.

Consistency is often used to describe the property of databases that transactions will only move the data from one valid state to another. In this context, it implies that none of the rules will ever derive results using values from two different times. Say we have a rich text editor with a margin and a tab length.

const margin = of(10);
const tabLength = of(10);
const tabPosition = from(() => margin() + tabLength());

The tabPosition rule must never be able to read a margin and a tabLength which never coexisted in time. This issue has a higher potential of occuring when reading from computed properties, like tabPosition. If the declarative system naively updates rules by recomputing any values that depend on a changed observable value, then its possible for a value that depends on both margin and tabPosition to receive an up to date margin and an out of date tabPosition before tabPosition receives its call to update.

Commutivity describes the property of an operation to not depend on the order of execution. Addition and multiplication are commutative, and so their operands may be reordered without effect. In code, commutivity describes the property of function to have the same effect regardless of which order they are executed:

mySet.add('foo');
mySet.add('bar');

In declarative programming, commutativity of the rules allows the declarative system to realize the rules in any order without affecting the results of the program. In CSS, the order in which classes are added to an element cannot affect the style properties which the CSS engine chooses to apply. First, the CSS engine will rely on the specificity of the rule’s selectors. Then, the engine will rely on the order in which the rules were written.

Collectively, these five components ensure that the order in which changes are applied to input data cannot affect the results of their logical relationship to derivative data.

Resources