Is It Imperative to Be Declarative?
blog.container-solutions.com
blog.container-solutions.com
if condition: a else: b
As equivalent to: if condition: do a else: do b
They might seem very similar, but they’re wildly different if you actually have the constraint. There is an implicit “do” somewhere in any program that has IO, but having it mixed in with a declarative program makes no sense and can’t make sense.In the SQL example, nothing happens if you reach the conditional and exit immediately. In the imperative example you have no way of knowing what happened without inspecting the resulting state, or the code that produced it.
I think they are always an artifact of imperative programming bleeding into a declarative framework.
In a fully declarative fashion, you would just have:
c where c is imperatively defined as being a or b depending on a condition/state.
A declarative if statement doesn't make sense.
Often your requirements on the final end-state involve some predicate logic. I want (A and B) or C. Using an Or makes sense, if you want your declarative logic to work on both windows and linux. Or you want configurable declarative logic, or you want to be easily able to manually change the files.
A very natural way to do predicate logic is "if A is true, then B should be true". That is logically equivalent to "B or not A". But letting people write "if A, then B" is just nicer.
Hence declarative logic is much nicer with "if statements".
Perhaps another way to view this is that many declarative systems "build" their description of the desired end-state imperatively. I wouldn't say that applies here, but I could imagine cases where you want to say 'for all elements here' which might be best solved with a for loop.
Which is fine in some cases. It is still control flow at a somewhat meta level.
But taking the example of a declarative definition of a UI tree, changing node identity using "declarative" if statements seems like a breach or even a smell from a declarative point of view.
The same way, for eachs are not necessary in the declarative layer. That can just be array iteration in the imperative/data layer.
if condition: a else: b
declaratively as a: condition
b: not condition
This rephrasing does not work if "do" is in the picture, and turing-incomplete parsing becomes difficult.Declarative way: c = condition ? a : b
Imperative way:
c = null
if condition:
c = a
else:
c = bAnd in Python at least, it would be:
if condition:
c = a
else:
c = b
(i.e. no upfront declaration of c)As being declarative is about expectations, one translation could be:
I expect that "either both a and the condition to be true, or b true and the condition false" once the computation done.
a when: condition
b when: not conditionThis is a definition after all.
c = a if condition else c = b
Whether c is implicit or not doesn't matter too much.
I think the comment you respond to is still correct.
x = (if c then a else b)
it is guaranteed that only x is affected. With imperative programming, you have to look inside the conditional to find what is affected. if c then:
x = a
else:
y = a
You could still make the above snippet work in a declarative way but, as I say, the targets are explicit x, y = (if c then a, y else x, a)
If you have function calls involved then the problem is even worse. In an imperative language, the following code could mutate x, y and any other variables arbitrarily (if the scoping rules allow it): x = 0
if c then:
f()
Whether this difference is enough to describe "if" as not "control flow" in declarative languages, I don't know, but that is not really an interesting question (just a matter of how you define the term).Edit: another difference is that I think declarative languages usually only use immutable types, so changing one variable is guaranteed not to affect another. Contrast with this Python snippet, where mutating x affects y.
x = [1, 2, 3]
y = [x, x]
x.append(4)> A declarative if statement doesn't make sense.
That’s exactly right (and was the point I was trying to make, though I can see how it isn’t necessarily clear, particularly with the pseudo code syntax I chose). But a declarative if expression definitely makes sense.
Let me rewrite the example as an s-expression:
(if condition
a
b)
In a lisp, this would produce a value which is either a or b depending on the value of condition. (It would typically be implemented as a macro so only a or b is evaluated, but that’s an implementation detail.)But an if expression is not the only way to declare conditions. For instance you might have something like:
(first
(filter predicate?
[a b]))
This isn’t exactly semantically equivalent, but it’s hopefully clear that it’s not imperative.Of course lisps are generally (though not always) closer to the functional paradigm than a more declarative paradigm like say logic programming. But if you look at say Prolog, you’ll find that it too uses conditions for definition (though again with somewhat different semantics).
It's okay but that's not fully declarative anymore.
Declarative is simply descriptive of the structure without specific build instructions if you will.
Let’s step back from “if” or “filter”. Suppose you have a basic primitive definition “book”. If you’re to define a taxonomy of types of books, you might start with “fiction” (or “non-fiction”, but this terminology kinda gives away where I’m heading). Okay, so a definition of a book which is fiction might be:
fiction-book:
book &
factual-constraints: none
So far we have entirely static definitions (at least within a universe that book-like things need no distinction). We don’t even have to define what could be assigned to factual-constraints.What happens when we try to define non-fiction? We have to define factual constraints it might have.
non-fiction-book:
book &
factual-constraints: all
There are some implied conditions here, and we could just stop there. If you have a book and want to know whether it is non-fiction, you must evaluate its non-presence in sets that don’t conform to full veracity (let’s be generous here and pretend non-fiction is honest, and omniscient).But let’s go further, and define so-called “hard science fiction” books (ie there are aspects which might be imaginary but only if the imaginary aspects are conceivable without disputing known facts, I know this is imperfect but I think it’s good enough for the purpose of this discussion):
hard-scifi-book:
book &
factual-constraints:
all - known-impossible
Uh oh, that’s an operation! Have we started imperatively defining types of books? Nope. We’ve declared how they’re defined, without specifying their inputs, and without affecting them. To the extent the definition itself is correct (exceptions already acknowledged), the definition will always be correct because it’s a tautology.But you can’t have the definition without those conditions, because a definition is a tautology. And that is the tautological definition of a definition.
What would make it imperative is if:
- the thing itself might change, and so the determination of its definition might change along with it
- the determination of its definition might change, and so its previous definition might have been invalidated
If neither of those things are true, all of the conditions expressed in definition are declarative.
I think it stems from the fact that you are only looking at expressions, forgetting that these are just part of, possibly implicit statements.
Basically, there is always an assignment. And that assignment is conditional.
That means that you are effectively writing how something is being built instead of simply describing what it is.
That is just not fully declarative.
That's all.
Pretty close. I’m only looking at expressions because that’s how you can achieve anything declarative. To the point that there are statements (implicit or otherwise), they can’t be declarative whether you have conditions or otherwise. Because…
> Basically, there is always an assignment.
If you’re restricted to only expressions, this doesn’t matter, because assignment is inherently definitional and idempotent. Assignment with that constraint is logically the same as equality assertion. In fact, some implementations of this concept will literally error if you try to change an assignment. As in:
a = b // okay, a is equal to b
a = b // yep, this fact has been established, carry on
a = c // if c isn’t equal, this will fail and nothing depending on a can proceed
> That means that you are effectively writing how something is being built instead of simply describing what it is.Now I don’t understand. How can you define things without constraining what they are? If you don’t have a semantic “not”, every definition you could come up with is inherently the infinite set, even if you define all of the affirmative facts. Without some mechanism to distinguish Thing A from Thing B, you have effectively arrived at two definitions of Things.
Note that the argument here is one of declarativeness vs imperativeness, i.e. whether it is at all practically possible to ever have programs that have zero need for control flows. It is not one of whether a() is a pure vs a stateful function.
- the language the programmer writes in
- the language that specifies the desired state of the infra.
You want the language you're writing to be a good language (ie, a language you are productive in). It's very helpful for the language the config is specified in to be a declarative manifest. That doesn't mean you as a programmer have to write that manifest language directly.
Pulumi and CDK are libraries that allow imperative languages to generate a declarative manifest.
Terraform is a (very hamstrung) declarative language that is also a declarative description of the desired infrastructure state. It is hard to use because the language is bad. But the language is bad for no reason because it confuses the very desirable property that you want a declarative config with the totally undesirable property that you generate that config with a crippled declarative DSL.
Ansible shows another side of this: it's a crippled declarative DSL that executes an imperative set of instructions to get the infrastructure into the desired state. The best you can say about the yaml straightjacket is that it causes you to organize your code with some structure, kinda like MVC web frameworks. But yaml's declarative nature doesn't impart any magic by itself. If you write a step that isn't idempotent, it just isn't idempotent.
If you find yourself wanting to add templates, and loops, and references etc to yaml: this is a smell that you should not be writing yaml. You should use a real programming language to generate yaml. That's essentially what Pulumi and CDK are doing. You can think of your real programming language (python or typescript or whatever) as a macro language that outputs the declarative config.
(For completeness, chef/bash deploy scripts are imperative languages that generate imperative commands to change the infra. Needless to say, this results in a mess usually)
Full disclosure: I'm at Pulumi.
I've done demos using our EKS provider to spin up a cluster, outputting a kubeconfig property and plumbing that into a Kubernetes provider to deploy a Helm chart to the cluster. It's 3 resources and fits on a single screen in most of our languages.
Thus I generally don't pay attention to claims about declarative programming — it is rarely actually attempted despite the frequency of advertisement. The domain which Pulumi / Terraform targets is most definitely too messy to attempt a pure declarative DSL.
The first option is great if you have time and resources so create your own provider. The second option is what you're more likely to use if you are small and don't have a dedicated team. Also it's convenient if you can achieve 99% with existing providers just need to run one command, but doesn't always work (Terraform has many limitations regarding this).
Secondly, I use TF and at least on AWS you use lifecycle hooks to do what you describe. If I saw someone using TF to do that,I would think they are incompetant.
The danger (and power) of the Chef DSL is it allows imperative Ruby anywhere, including shops that liberally use monkeypatching "to put out fires".
Puppet is a custom DSL that doesn't have this problem but it lacks the power of Chef.
Declarative is generally even more limited than DSLs like Puppet and can't support complex logic. It maybe worth externalizing complex logic into "helpers" or other systems, but this creates a "Javascript frontend + something else backend" code-synchronization problem.
Declarative seems "easy" and "looks like data" but it rapidly runs into limitations.
(The other confusion with Chef is without unified_mode, there are 2 main execution phases that run on clients: compile-time and converge-time. unified_mode makes it easier for novices whereas 2 phase allows for the compile phase to adjust node settings when using an "API" model without databags or run_state.)
Functional and logic languages would disagree.
For configuration, have a look at https://dhall-lang.org/
YAML is a terrible foundation for a declarative config. You need a language designed for declarative config.
Some commenter above said that this space is too messy for a purely declarative language; someone else said Pulumi should be thought of as a macro language to generate a declarative spec.
Instead of bolting on a whole Python interpreter to work as a macro language to generate a theoretical declarative spec, why not instead just write a declarative spec in a sanely declarative language?
It's not that hard to learn Nix. Stop procrastinating, everyone, and go learn Nix :)
I feel the same about CMake. It's horrendous and everything would be much simpler if you could use the full power of an imperative programming language. Conan on the other hand is Python-based and essentially a framework (though for a different purpose). With a nice set of built-in functions and a clean interface design, I believe that using a full programming language gives you many of the advantages of declarative languages without the oddities and an easy way to drop down to a lower level.
Declarative is when the interpretation or execution of a declaration in a program does not depend on the order in which you execute those declarations.
Say you have 10 pure functions. The program --its interpretation-- is the same no matter how those functions are ordered in your source-code . The functions can depend on each other but no matter the order in which you call them the results are the same.
But if those functions have side-effects, then the order in which you call them can have an effect on the end-result. It can cause an error if some initialization is not done before it is needed. That is not declarative programming then.
You can't actually reorder these statements and expect the same UI tree.
Usually, declarative is built upon imperative and simply means that implementation details have been abstracted enough to the point that we can build the program by describing its structure and it will build itself. As opposed as defining the build steps (they are predefined if you will).
Defining insertion would be in the imperative portion of the code right?
Since there, source order matters, you would have to change the subroutine that triggers insertion by changing the direction?
When describing the tree structure however, listing the nodes (html nodes for instance) has to be in order, no?
Creating those nodes can indeed be reordered provided there is no interdependence.
the same way
var a, b; c =a + b;
can be rewritten:
var b, a; c =a + b;
Asking because it seems to me that commutativity is a hard sell here. Might be mistaken about what you mean.
It doesn't matter to me how this HTML is turned into a DOM. You could go through the HTML line by line and create the nodes. Or, if you'd think it could be faster, you might insert the children in reverse order at the correct position (e.g. start inserting the last element, then before that insert the second-to-last, and so on). All that matters is that the DOM in end behaves exactly like the one I specified.
In the end, it's the same as with SQL. It doesn't matter what exact approach you take to get me what I want, as long as the end result is what I specified.
For html, the renderer still depends on the structure of the tree.
Whether it starts from the bottom of the top doesn't matter. Because the renderer is an imperative function.
But the declarative part is the tree itself and these declarative statements are not commutative is what I mean.
You cannot insert to a node that doesn't exist.
> For html, the renderer still depends on the structure of the tree.
Of course! But it's not important whether you first insert node A at place X and node B at place Y, or the other way around.
> Whether it starts from the bottom of the top doesn't matter. Because the renderer is an imperative function.
See, that's the thing. What renderer are you talking about? I'm talking about the process of turning the declarative HTML tree into a DOM tree.
> But the declarative part is the tree itself and these declarative statements are not commutative is what I mean.
Well, no. You can't change a declarative structure and expect it to be the same. But why does that have any bearing on whether HTML trees are declarative? The tree is a declaration, and it doesn't matter how you turn this declaration into a DOM tree.
It seems like you're expecting to be able to re-order the child nodes. That is not what declarativity is about. It means that it doesn't matter in which order you do the operations. The operation itself is defined by the node placed into the tree AND the resulting position of the node. You are right that you can't re-order the node declarations, but you can execute them in whatever order you want.
Seems then to me that it is exactly the example of declaring variables. The order doesn't matter indeed.
But for operations on these variables, i.e. function calls, there needs to be an ordering between variable declarations and functions calls (source order) except in a few cases maybe where a language uses hoisting.
That's in the simple case where there are no side effects (but the parent comment takes this into account already).
Of course, when someone transforms an html element into its corresponding DOM node, because this does not depend on shared state, these operations can be commuted but that's not what declarativity is about.
Declarative is: cook me a chicken.
As opposed to imperative i.e.: turn the oven on, wash and season the chicken, put it on the oven, wait 1hour, get if off the oven and serve it.
You can buy the chicken or the oven whichever first. Still need to follow the steps in order.
I'm still not quite sure I understand what you meant exactly :/
But what if the web-page shows a cookbook recipe whose steps are elements in the DOM-structure of the page? Such a page then procedurally describes how to cook a cake, right? Is that recipe then declarative or procedural?
A recipe is a description of the steps to the end result. So it's imperative.
Here instead of DOM nodes, you probably have utensils and ingredients on which you apply operations (grab, heat, pour oil etc...)
The end goal is not the description of the recipe itself. It's the meal that you get once you have followed the steps to completion.
Yeah, so it does not matter in which order you render them, as long as they are inserted/shown in the right order in the end.
You wouldn't be able to modify something that has not be rendered yet for instance.
Render itself is declarative perhaps, but that's the meta level.
Render is imperative in terms of rendering a declarative tree.
What does rendered mean? In the browser window? Why not, you can certainly modify that. If you mean not rendered in the HTML then we are now talking about two different things, because my answer was meant for when there is an existing HTMl tree already.
But that's a red herring.
The point in that the sequence, render-modify cannot be turned into modify-render. This is not commutative.
Now, render(a, b, c) where a, b, c are html nodes is a slightly different story because we consider only one function.
Here, your claim is that since it is basically equivalent to render(a) + render(b) + render(c) then it's commutable.
Well, it depends at which granularity one looks at it (subtrees...). And as the original comment mentions, in the general case, it's only true if there is no side-effects.
In other words, if you, currently, put some html elements (whatever that means, maybe using innerHTML or so) is there any guarantee how exactly they are rendered, meaning, how they are turned into dom nodes?
Because of not, then render(a, b, c) is in fact equivalent to render(a) + render(b) + render(c).
If this process can be controlled, then obviously "inserting" an html element (structure) must be able to describe that process to make it declarative.
That render decomposition works for sibling elements.
However, when there is a parent child relationship between html elements, the child can only be inserted on an existing parent.
Basically, render(a(b)) is not render(ab) where a(b) denotes that the html element a depends on the html element b for its definition.
This is a new example. How does that contradict any of the previous points that I (and the OP) made? I fail to see that, can you explain?
Some people believe that pure functional languages are declarative because of the analogy with mathematics — computations are determined purely by the statement structure, not by the ordering of the statements.
Some people (including me) have higher requirements — the declarative programming language should never tell what to do, should only define the problem without providing an algorithm to solve it. According to this definition, pure functional languages are far from declarative as much as the procedural languages. The only pure programming languages here (AFAIK) are some variants of logic programming (Answer Set Programming, Problog, ...), Constraint Programming (MiniZinc, Essence, ...), PDDL, some configuration languages (not all of them), etc. There is nothing wrong in having conditionals/loops in the declarative language, as long as they are used to define the problem, not the algorithm to solve it.
This process will probably never end, as we build more and more complex systems we will find new abstractions that better fit at modeling the state of the systems, and we have to go through that process of redefining the declarative models on an ongoing basis.
If you look at the Kubernetes world, the same pattern played out. You have a declarative layer that is becoming ever more complex and hard to manage using tools like Helm, but then there is also a trend of encoding imperative operational knowledge in new frameworks and operators, that expose a much simpler PaaS declarative layer to users.
I wrote https://devops-pipeline.com/ it toplogically sorts a dot diagram file and executes a Graphviz diagram. In theory your code should produce an entire environment that is ready to use.
It is infrastructure as code and pipeline's as code. It also runs parts of the diagram that can run in parallel in parallel
Creating robust stateful code is an exercise in frustration. Just look at any command line utility that manages state of files or EC2 instances. If you run something and it fails then the files or infrastructure Is in an unknown state. This is the problem that both declarative and imperative tools have
F(n) = 1 (n = 1)
F(n) = 1 (n = 2)
F(n) = F(n - 1) + F(n - 2) (n > 2)
But the above definition says nothing about how you should calculate the value of F(n). As a matter of fact, math tells us that the definition of F(n) can be converted to a formula that doesn't include conditionals. In other words, the conditional in the original definition of F(n) has no direct relationship to the actual control flow of programs that calculate F(n).It follows that F(n) is defined declaratively, invalidating the claim that declarative expressions can't contain conditionals.
[1] - https://existentialtype.wordpress.com/2013/07/18/what-if-any...
> This can be reduced to:
> > “Imperative programs make a choice about what code is to be run.”
No. You left out the part about order, which is the most important part.
One analogy that popped up in my mind is sorting: if you can provide an answer to a choice between any two options in a list, in effect you can use that to order the list.
another is programming with DAGs, the DAG captures control flow schematics as a value, abstracting away the actual evaluation rules (maybe the DAG is meant to be async, or reactive, or distributed). The DAG is declarative with respect to the abstracted evaluation rules, yet if there are effects in DAG nodes, we are not declarative with respect to those. We are however declarative with respect to how effects with a lifecycle are maintained and garbage collected, as that behavior is given by certain runtimes for the DAG.
The thing I actually care about is: can I use a programming language or not?
Most real world situations are unique and require unique solutions. That's where AI really shines. You just describe your target, attempt to solve the problem, and pay attention to how far off you were from your target. The learning happens naturally.
Neural networks are too complex - sometimes billions of variables - to decide what each neuron should do. We as a species have evolved to develop brains that are extremely adaptable. AI mimics our own natural learning process. And it's proven to be far more effective at solving unique problems.
An extraordinary claim, I think. Source or evidence?
While at it, come back and tell us when you've implemented a neural net using AI, and not a programming language, come back and tell us too.
Also, training a neural network can make it worse; it's actually the combination system of the model and its engineers that makes it improve. (https://en.wikipedia.org/wiki/Catastrophic_interference)