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.
A declarative statement has five properties:
Reactivity describes the property of a declarative system to observe the input data for a rule in order to reapply the rule whenever they occur. To deliver Reactivity, a declarative system must be able to determine what data the rule ingests during its realization, either statically, dynamically, or implicitly.
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 equivalent value given equivalent 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 equivalent side-effects given an equivalent program state, so long as the program state that can alter the rules effects be observed for changes. Any values which the rule ingests 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:
const mySet = Set();
// mySet will change:
mySet.add('value');
// mySet will not change:
mySet.add('value');
In the case of imperative programming, you may say that the second call to add value
to the set is insignificant, since no change occurred, but you could also say that once the second call to add value
was made, any previous call to add value
was insignificant, since their change or lack thereof became inconsequential.
This insignificance relies on the arguments being equivilant, however, as a new call with a new value will still have an effect.
const mySet = Set();
// mySet will change
mySet.add('value');
// mySet will change again
mySet.add('other value');
Strong Idempotence pushes idempotence further by removing the condition of matching arguments. A strongly idempotent function will render any prior execution of that function irrelevant, regardless of which arguments were used:
let number = 0;
function wipeout(arg: number) {
number = arg;
}
Strong Idempotence ensures that prior executions of a declarative rule 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 occurring 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.
Commutativity 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, commutativity 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.