Hacker Read top | best | new | newcomments | leaders | about | bookmarklet login
Is It Imperative to Be Declarative? (blog.container-solutions.com) similar stories update story
57 points by zwischenzug | karma 4169 | avg karma 4.19 2023-04-01 07:32:51 | hide | past | favorite | 76 comments



view as:

Echoing the article, declarative is a degree and almost all if not all DSL's do not achieve declarative purity. The best places for a declarative DSL are for domains which are very elegant and well-studied, such as relational databases.

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.


Terraform has a non entirely declarative layer which generates entirely declarative representation which is then applied using providers. This may be a tenious difference from imperative, but it is the crux of it and this is what you want when managing resources.

But it's also limiting, for example, you may want to run some perform an action before a resource is deleted and then you have two options: a) create your own provider that will will handle the provisioning and deprovisioning under the hood b) use provisioner (e.g. local-exec)

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).


Uhh, why would you ever use Terraform to do that? Its wild seeing people try and force Terraform to do things that the docs more or less YELL at you to not do.

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.


Not every provider has lifecycle hooks.

Right, and (as I understand it) vendor neutral is a selling point of Terraform

In terms of DSLs, this is the crux of Puppet vs. Chef.

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.)


> Declarative is generally even more limited than DSLs like Puppet and can't support complex logic.

Functional and logic languages would disagree.

For configuration, have a look at https://dhall-lang.org/


Nix is fully declarative (with flakes), supports complex logic, and is more powerful that chef and puppet. It is a bit more difficult to learn for those reasons as well.

+1.

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 :)


YAML is designed for declarative config.

Given that it has none of the features that make Nix bearable, and (if my long memory serves) has repeatedly been busted for truck-sized security holes, and, on top of that, has a grammar that makes me long for that old gif of a hexadecimal keypad, because at least hexadecimal is clear, I'd say it failed, badly.

I think this strikes at such a great line of reasoning: declarative vs imperative is the wrong abstraction.

The thing I actually care about is: can I use a programming language or not?


When a problem can be solved mindlessly - with a repeatable set of steps no matter the situation - imperative/declarative programming makes sense.

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.


> [AI] has proven to be far more effective at solving unique problems

An extraordinary claim, I think. Source or evidence?



When you've built a product using only OpenAI, and not a programming language, come back and tell us.

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.



You don't think code is involved in that my dude? The input wasn't a description into an AI model, with copilot the model. It's code that calls to an ML model. Which is my point. The model was created with code. The model is deployed with code. It runs on infrastructure that executes a bunch of other code. Code takes in user input, puts it into the model, takes the resulting response, does something with it. There is no ML model that is a product in and of itself. ML is not a compiler. It is not a runtime environment. It does not understand business needs, it does not take direction. It certainly may be transformative, for good or ill, but it hasn't suddenly deprecated the need to code; far from it.

Neural networks explicitly don't work the same way as human learning; they don't have online learning, humans definitely don't learn through backprop, humans have memory and compute in different parts of the brain, etc.

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)


Sigh. Declarative condition expressions are not control flow, they’re part of the definition. They are part of “what” and you could not define anything meaningful without them. The mistake is treating:

  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 don't understand how it is not control flow.

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.


In declarative systems, you want to explain the final end-state.

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.


I understand. But as you seem to hint at, it seems that "declarative" logic is still an imperative concern due to insufficient specification of the declarative framework.

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.


You can represent this

    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.

This is incorrect. You are not defining a or b. You are defining c as either a or b.

Declarative way: c = condition ? a : b

Imperative way:

    c = null
    if condition:
        c = a
    else:
        c = b

But in many imperative languages you would equally use a ternary operator? c = condition ? a : b

And in Python at least, it would be:

    if condition:
      c = a
    else:
      c = b
(i.e. no upfront declaration of c)

I would not say this is incorrect as there is no definition of `a: condition`.

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 condition

There must still be an assignment somewhere.

This 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.


You can say that declarative style is only constant assignment. It doesn't make sense to use = though because (unlike imperative style) assignment is all there is, in a way

I think the main difference is that, in a declarative statement like:

    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)

Plenty of good answers already, but I’ll add this to clarify:

> 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).


filter is an operation. As soon as you add an operation that builds the tree, it's imperative.

It's okay but that's not fully declarative anymore.

Declarative is simply descriptive of the structure without specific build instructions if you will.


It’s an “operation” in the mathematics sense, but it’s not an operation in the imperative sense. It doesn’t do anything (ie it doesn’t have an effect), it is part of the description.

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'm not too sure of what you mean.

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.


> I think it stems from the fact that you are only looking at expressions, forgetting that these are just part of, possibly implicit statements.

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.


Assuming a is an expression, which could well be a call to a function, then the two are equivalent.

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.


Here's a definition of "declarative program" I would start from, (trying to come up with a better definition)

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.


Not really sure what you mean. Alledgedly a HTML tree is declarative but sibling elemznts are ordered.

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).


I think you're mixing up the data with the statements. Yes, nodes in a subtree are ordered, but that doesn't mean you can't re-order the statements creating those nodes. For example you could insert the nodes in reverse order at the correct position. It wouldn't make any difference to the tree itself.

I must be a bit dense this morning :). I'm still not sure I understand.

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.


I think there might be a difference in what we see what the HTML tree exactly is in this example. For me, the HTML is a declaration of structure. This can be passed to imperative code to build the DOM, but the tree itself (the HTML code, if you will) is a declarative representation of what I want to have in the end.

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.


Yes, it's an abstraction over the implementation details.

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.


I don't think what you write actually touches what I wrote.

> 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.


Just to be sure, what do you mean by executing node declarations?

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 :/


I'm still a bit confused about the distinction. A HTML-page would be a declarative definition of a DOM-structure, right? It just declares what I want to see in the browser, it does not describe how the browser-engine shall render the HTML given to show it to my readers. Good so far, declarative.

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?


The html code is a declarative definition of a webpage. The internal representation as a DOM tree is just the imperative implementation on the backend.

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.


But my end-goal could be finding the recipe, maybe I am a recipe collector.

> Alledgedly a HTML tree is declarative but sibling elemznts are ordered.

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.


That works for a single function call.

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.


> You wouldn't be able to modify something that has not be rendered yet for instance.

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.


Rendering meaning turning the html elements into dom nodes first before painting.

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.


Well, now we are really down to semantics. I don't think there is any guarantees about rendering whatsoever, are there?

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.


The thing is, this is simplistic example. It's not just about pure semantics nitpicking.

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.


> 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?


TIL that Prolog has loops. I can't imagine how you would use them, though.

This article doesn't really get at the core of the distinction between tools like Ansible, chef, terraform and pulumi/AWS CDK. There are two languages being discussed here:

- 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)


Pulumi does not generate a declarative manifest, your infrastructure program runs at the same time as the engine which applies resource operations. This allows you to use API calls, sleeps, and so on. It's a much more powerful model, and lets you use arbitrary computations between resources, using the output state of one to make an API call or some side effect, and plumb the result of that into the input of another resource.

Full disclosure: I'm at Pulumi.


But Pulumi still uses Terraform providers for many resources? And isn't it just as difficult to have multiple providers that depend on each other in the same stack?

Not exactly, it's a little more nuanced than that.

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.


You call that "powerful", I'd call it a nightmare.

Ah, ok my mistake. I was under the impression that Pulumi worked like CDK (except compiled to terraform) and wasn't imperative.

> > “In computer science, control flow (or flow of control) is the order in which individual statements, instructions or function calls of an imperative program are executed or evaluated. Within an imperative programming language, a control flow statement is a statement that results in a choice being made as to which of two or more paths to follow.”

> 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.


Just nitpicking here but doesn’t the property of being ordered follow from being able to choose between options at any instance?

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.


That's an unrelated meaning of the word “order”. In this case, we have statements that all get executed, but in a specific order. There's no choosing between anything. What you're referring to is ordering in the mathematical sense, which means defining what is “bigger” in a certain sense than something else.

This post assumes that there is consensus around a formal definition of being declarative. This is hardly the case as highlighted by Robert Harper, an academic recognized for his work in the field of programming formalization , in his essay "What, If Anything, Is A Declarative Language?" [1].

[1] - https://existentialtype.wordpress.com/2013/07/18/what-if-any...


> For various reasons, Terraform was ditched in favour of a python-based boto3 solution, and one of those reasons was that the restrictions of a more declarative language produced more friction than the benefits gained.

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.


In the Java world there is also the debate between Maven (more declarative) and Gradle (less declarative, but also tries to pass as declarative), and the same conclusion is always reached: Maven make it very difficult to do complex things, therefore Maven builds tend to be very simple (sometimes lacking obvious features because of that) while Gradle projects tend to become utterly complex as it's just easy to use Groovy or Kotlin to create whatever mess you need the build to do... my argument to defend Gradle when this is pointed out is that if the build is doing intrinsically complex things, having a proper language to manage that complexity is the only sensible solution: people writing these builds are professional developers, if they can't keep complexity under control using a fully featured programming language, then they are just not very good professionals.

I wanted to be capable of bringing up an entire environment with one command using an orchestration of Kubernetes, Terraform, Ansible, Chef and Packer.

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


proposition: declarative computation is one that is compiled or interpreted into something with lower control flow. consider lisp: your DSL can have control flow if the macros process the control flow forms (if, lambda) into some lower thing (that lower thing often being lisp itself but after all macroexpansion). I.e. reactive DSL is declarative with respect to when recomputation happens and when memoization happens, the programmer does not specify these aspects they are implied. however the same DSL can still be imperative with respect to effect sequencing.

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.


I think the article dances around the real issue when it comes to the declarative vs imperative debate. The discussion should always take into consideration the complexity of the system, and its dynamic nature. Systems tend to grow in complexity over time and the declarative constructs that were once enough to describe the state of the system start to require ever increasing complex hacks that resemble imperative code. At that moment the declarative interface for the system should be redesigned, with the new complexity moving into the imperative core, once again making the declarative interface/shell be congruent and simple.

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.


AFAIK there is no single generally accepted definition, what does it mean for a language to be declarative.

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.


The article asserts that anything that includes a conditional must be imperative because it dictates control flow. Under that view, the definition of the Fibonacci sequence would be imperative.

   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.


Legal | privacy