What is declarative programming? Declarative programming is not concerned about implementation; declarative programming is concerned about what we need to act, what we need in terms of ‘behavior’ to provide an accurate answer to a question.
Instead of asking how to achieve something, the declarative approach, closely affiliated with functional programming, is committed to what needs to be done.
Let’s try and see what happens if we speak about the declarative approach, not in terms related to the world of programming but through the philosophy of language and the view of late Ludwig Wittgenstein.
Early Wittgenstein attempted in his seminal treatise Tractatus to package reality and its relation to language in an orderly, hierarchical fashion. In a beautiful but vain attempt, Wittgenstein produced a model that would correlate mind, language, and the world as we perceive it through language and the mind. Wittgenstein himself was later on not very satisfied with the solipsism that followed.
Wittgenstein thought of the world as the totality of all facts ‘being the case’. The world is built by small atomic units, objects, simple and unanalyzable, only possible to emerge as parts of a state of affair, as part of a composition. Language is a reflection — a mirror — of the world sharing its logical form. Names mirror objects, elementary propositions mirror facts. And so on, building a jigsaw pattern of things, language, and the perceived world.
In this view lots of things we speak about — everything that cannot be depicted as a state of affairs — are meaningless. ‘Whereof one cannot speak thereof one must be silent.’
Some claim there is a great rift between the approaches in Tractatus and the Philosophical Investigations and other notes. But I’d rather want to see Wittgenstein’s later works as a continuation. In broad terms, late Wittgenstein asks: why is it that we can use language to describe logic, propositions of science, and have ordinary conversations? The former categories loosely speaking self-contained (or so they seem), while the latter category is vague and at times illusory.
What do we do with words?1 Suppose everyone had a box with something in it: we call it a “beetle”. No one can look into anyone else’s box, and everyone says he knows what a beetle is only by looking at his beetle.—Here it would be quite possible for everyone to have something different in his box. One might even imagine such a thing constantly changing.—But suppose the word “beetle” had a use in these people’s language?—If so it would not be used as the name of a thing. The thing in the box has no place in the language-game at all; not even as a something: for the box might even be empty.—No, one can ‘divide through’ by the thing in the box; it cancels out, whatever it is. That is to say: if we construe the grammar of the expression of sensation on the model of ‘object and designation’ the object drops out of consideration as irrelevant. — Ludwig Wittgenstein in Philosophical Investigations
When we use the word ‘beetle’, we use it to achieve something, speaking to others. If everyone acts and say things as if the beetle is a small bug, would it matter what people had in their boxes? What it even matter if someone lied? Would it matter if someone had a grasshopper in their box?
No, not while playing a move in this particular language-game. Words have meaning when used in actions to produce an assumed effect in communication. Language is not private.
In functional programming, functions are ‘black boxes’. We are concerned with this: every input to the box must produce the same output. If this is not true the function is not pure, we say.
What happens inside the box?
It doesn’t matter all that much. If the function is pure, we can count on it, this time and the next. And if we give it a label, a name that declares an intent, we can use this label to reason about what we need to act, what we need to achieve an answer to a question. In some respects, it doesn’t even matter if the function is implemented. We can still reason about what we need to achieve some effect.
The purity of the function could be thought of as inside and outside, or two sides of the same coin.
const f = (...xs) => xs.reduce( (acc, v) => acc + v, 0 );
f? Obviously, we have written a function that returns the sum of some values. But
f is not a label we can reason about and we could write all the tests and produce formal proofs in the world, a function labeled
f would still not a function we could use to reason with. Its label is non-declarative. In fact, we would have to read through the (in this case short) function to understand what we can do with it.
const sum = (...ns) => ns.reduce( (acc, v) => acc + v, 0 );
A function named
sum such as this one, on the other, lends itself to reasonings with no cognitive energy investment in the implementation. The intent is clear, and we know what needs to be done if used.
A black box? Does it matter if the beetle is inside? Yes, the implementation does matter. On the other hand, it is not as important in the context of business logic. When reasoning about how to solve problems, what needs to be done ‘to answer a question’.
Language is not private, therefore a function that sums numbers does not respect this with the label
f. Avoiding assignment at the cost of readability is to treat code as being private. But code is supposed to be communicative.
This is why functional programming advocates small functions with a single responsibility. When they have a single responsibility they are easier to implement, to test but also to name. And with a communicative label, it’s easier to reason about the function, combine it with other functions, and thus building a composite, more advanced function with greater specificity. Much like we communicate and construct arguments using the words of natural language.