Not sure I buy that logic. With that reasoning, all code is declarative because it's "declaring to the interpreter/compiler" how to operate.
I think he's making the same point you did. In one case, the config file is laying out a sequence of steps like you did for how to make a burger (imperative). In the second case, the file just defines the end state (declarative).
Sure, you could say that the first case is declarative because the "end state" being defined is pipeline itself. But the point is that we're talking about how to get a burger, not define how to make one.
Sort of! This is a perfect example of how useless the whole "imperative vs declarative" distinction is. Nearly everything in a computer is both imperative and declarative, in some fashion, at some point.
These terms were not made to be some concrete and inviolable paradigm of computing. Some academics wanted people to write programs where you didn't have to spell out every instruction to the compiler, so they made this crude distinction. Things like a function called give_me_a_temporary_file(), rather than writing out an entire function yourself to create and return a temporary file. But both are executing steps imperatively under the hood.
The differences that I'm pointing out are 1) declarative does not describe a flow, the flow is under the hood; and 2) configuration files do not actually perform steps, they merely describe arbitrary data, and only the program interpreting the configuration can determine if the result is imperative or declarative. For some programs, the exact same configuration file may result in either imperative or declarative execution.
And I don't think he's saying they do either. In fact, I don't think the post gets into execution details at all! If you go through it again, he's only talking about ways of writing config files, not ways of running them. In one approach, the config defines the pipeline itself. In the second approach, the config defines your desired end state.
The "imperative" vs "declarative" distinction is entirely dependent on what your goal is. If your goal is to write a very specific pipeline, then the former is also declarative! But the context of the article is in achieving some desired end state in CI. With that context in mind, the former is "imperative" and the latter is "declarative".
I get that the whole story is saying "do things declaratively". But I think that term, and its ability to be misused (as in the quoted example) are distracting, because we get lost in the weeds and miss the real point, which is that we shouldn't be writing pseudo-programs in configuration files. I think we can all agree on that; so let's just say that, and leave the magic words alone.
It has nothing to do with "writing programs in your config file". The problem is that your config file is telling the runner how to reach the goal rather than what the goal is.
The real-world alternative is a partial rewrite of your CI infra, which usually not feasible.
This is essentially what I think, and I've thought for a long time: https://news.ycombinator.com/item?id=3507281
To the extent that people say "But what about..." my answer is that there isn't a particularly useful line to draw between imperative and declarative. There is one; I can draw it too. I challenge its usefulness. Imperative things have too many declarative things mixed in, and vice versa, in practice for it to be a very useful metric. I find what I mentioned in that post about the ease of debugging to be the real information I get when someone uses the "declarative" phrase; I can pretty much count on things breaking and me being unable to fix things whenever I see that word used.
I find it much more useful to mix things up as appropriate and not sweat which things they happen to be. A "declarative style" is a useful tool to be used, little more, and it almost never belongs in any sort of pro or con list. The pros or the cons should be at the next level down, like, "it's hard to debug" or "I'm typing way too much for what I'm trying to accomplish". I haven't evaluated any techs and given or subtracted points merely for being or not being "declarative" in a long time.
All code is also imperative, eventually, at the machine code level
But such a reduction is not useful. That's the same as stating that all computers are analog at the electrical level, or that the Internet is a series of tubes at the physical level.
Things like a function called give_me_a_temporary_file()
This seems purely imperative to me. A declarative statement would be (exists temporary_file). But that is a bad example anyway, as I struggle to find a good use case for why the end state of an instruction set would be the existence of a temporary file. If your goal is to perform subsequent steps with that temporary file, you're working with an imperative interface regardless of how you write it down.
I sort of tried to do this with the Factorio terrain generation system[2]. The first phase is to run a Lua program, which is imperative, but the result is an immutable object representing the terrain generation configuration, which in turn includes functional expressions, since a map where everything is constant would be boring.
[1] I f&!@#^ing hate Gradle; it is my go-to example of thoughtlessly mashing paradigms together because you can, resulting in something that nobody I have ever met really has really been able to work with except by trial and error.
[2] See https://factorio.com/blog/post/fff-200 and https://togos.github.io/togos-example-noise-programs/
The more accurate people's words are, the clearer they can communicate their ideas. Better communication leads to better results.
To give an example familiar to this forum: imagine we’re building a package. This can be set up imperatively with a shell script, or declaratively with GNU Make. Make will figure out which output files are missing or older than their input files, constituting a difference. It will figure out (topological sorting) an admissible path through computing a new up to date file from input files that are already up to date (re their own input files). Now that’s cool and all. But the same end result can be achieved way simpler if you can skip the diffing bit. If you assume you’re doing a clean build, you can order your build steps optimally in a shell script, skipping the now useless complexity. If there has been an abandoned rub that had some successful steps, your shell script will waste their results, but still end in a correct state.
Whether the system charged with realizing desired states reasons about the current state is actually not relevant. Declarative systems work by generating the desired state 'from scratch', often with some kind of efficient cache.
A declaratively styled configuration system which still works by transitioning the system between target states statefully is still subject to the same problems as imperative configurations, but they're abstracted away from your field of view, as long as things sort of work.
A stateless approach, also typically (always?) configured in a declarative style, actually addresses these problems in a principled way. For an example, consider NixOS vs. Puppet, Chef, Ansible, etc.: https://www.domenkozar.com/2014/03/11/why-puppet-chef-ansibl...
I like that essay's term for the paradigmatic differences, rather than the merely stylistic differences between imperative and declarative configurations: convergent vs. isomorphic.
What do you mean with "stateless" here, and which of those meet the stateless criteria? I have a hard time imagining any of those without state.
Deployments of server applications do usually involve some inherent state, like the contents of a database or the fact of which services are running. In that respect, some deployment tools in the Nix ecosystem are not isomorphic. But the software management and package management components, for example, are declarative without involving any reasoning about the current state. It is in that sense that those components are 'stateless' in a way that the rule of thumb proposed in the GP is not.
This is an option, but it is certainly not a requirement. If I say "I want cheeseburger", it is irrelevant to me if the cheeseburger is made from scratch or had been partially assembled ahead of time.
You are concerning yourself solely with configuration that can be generated not only declaratively, but also fully deterministically, but configuration systems can indeed exist where the environment isn't sufficiently deterministic. In such cases, taking the current state of the world, the user's request, and then determining the appropriate (and preferably minimal) actions to take to harmonize those without having to have them specified by the user is a declarative approach.
For example, I can go to the AWS console and turn up a job with a set of parameters, or I can have a configuration file and a daemon and if
{
cpu: 5,
mem: 20,
image: "my_docker_url",
}
is present in that file, I can then assume a job meeting those specifications exists, and if not present the job will not exist. The daemon might do this by shutting down all the jobs and starting new ones every time the file is parsed, but that is probably not great. Approaches that only restart changed jobs, or which can dynamically resize jobs that support such things might lead to significantly better performance of th esystem as a whole. import cc, ld, glob, make
// make defined as:
fn make(to, from, how) {
if (exists(to) && mtime(from) <= mtime(to) {
return
}
how(to, from)
}
make("foo.o", "foo.c", cc)
…
make("a.out", glob("*.o"), ld)
It’s basically the same as Makefile, except that out “make script” doesn’t know about goals anymore, only steps to take. Do we need that goal-knowledge in a declarative format is an open question, because you can wrap these make() calls in a function named aout() and that becomes your declarative goal. However declarative your config/data is, you still pass it to some evaluator like `make` at the end of the day.The Makefile is a (default) goal and a sea of possible actions from which make creates a plan of action.
The shell script steps are manually ordered by the programmer.
Well, yes. Those words are not as well defined as common usage implies. Their meaning is completely dependent on what you define as your basic operations.
People usually agree on some large spaces of incomplete definitions, so the terms aren't meaningless without context. But never everybody agree, and never most people agree on all the details.
Anyway, configuration files is one context where people have very diverse concepts of basic operations, and so most people almost never agree.
My Emacs config would like a word with you. :D
Clearly you haven't seen the gradle files I have been subjected to.
> ssstttrrreeeeettttttcccccccchhhhhhhhhhh
apt
Except that mayo should go on top.
But forget about the mayo...regardless of where one stands on proper mayo position there is a much bigger issue with the imperative cheeseburger instructions given: there was no cheese!
My babel.config.js would like to disagree.
The idea between having a declarative configuration vs a traditional one is well understood in the context of a machine. In one case, you describe how you want the final machine to look like and let the system decides how to reach that states, in the other you yourself input how the state will be changed step by step to reach the final stage. Sure the declarative configuration really is a DSL and there is an interpreter somewhere turning it into a step by step list of actions but that’s invisible to the end user.
It’s not magic just abstracted. That’s the good old Wheeler aphorism: “ There is no problem in computer science that can't be solved using another level of indirection.”
Develipera make fun of how project managers call anything Agile without understanding what it means, then go and commit the same sin themselves.
Declarative in practice usually means that an intelligent solver works backwards from a description of a final state
Make a file is imperative. There should be a file here is delarative.
Making a file may fail depending on implementation if it already exists. "This file should exist" likely will not because it will do whatever is needed, including nothing, to ensure constraints are met.
The best configuration file in my opinion, though, is to keep it trivial, and just have one factory config with very few options.
Need to log in and think you need to set a username and password? Linux has that already, lots of apps use the user accounts instead of their own nonsense layer or even directly use SSH as their transport.
Need to enable optional modules? Can you just make them enable and disable themselves on demand?
Network settings? I hope there's at least the option to just discover everything automatically.
> Before you say "what about template files! jinja2! go templates! hcl!", that is merely a DSL, which is no longer a configuration file; it is effectively a program in a crude programming language, interpreted by an interpreter (the program loading the file).
It will devolve into messy "imperative" code writing "declarative" "configuration"? or whatever using templates, so there's no point in being a purist about The One True Way.
Is there a reason it couldn’t be “Give me a cheeseburger”? This declarative camp narrative kills me sometimes.
...or procedural programs while we are at it.
I don't disagree with your argument, but the definition can be extended in this use case. If we assume the "configuration" of the software is the target destination. What you can configure imperatively perhaps is in contrast to that what roads to take or not and I believe the term fits reasonably well without it losing precision.
There is no such thing as an imperative or declarative "flow". By its very definition, declarative does not have "flow". It is just a statement. Imperative (in programming) literally means "describing steps that change state". Declarative (in programming) literally means "describing a state which is desired".
Declarative would be "I want a cheeseburger." Imperative would be "Get me a bun, and some lettuce, and tomato, and mayo, and raw meat. Cook the meat on a grill at high heat, flipping half way. Put the mayo on the bottom of the bun, then the meat, then the lettuce, then the tomato, then put on the top of the bun. Give it to me."
It's still strange to me how people learn about "magic words" like declarative and imperative, and then try to ssstttrrreeeeettttttcccccccchhhhhhhhhhh their meaning into some new paradigm that they have just thought up.
There is no such thing as an imperative configuration file. A configuration file describes how a program should be configured. Even when the configuration file "describes a series of steps to change state", it's still declarative, because the configuration file is still declaring to the program how to operate. It's the program that is imperative or declarative, depending on how it interprets and acts on the configuration file. (This is made clearer in programs like Ansible, which, within the exact same configuration file, supports both declarative and imperative statements)
Before you say "what about template files! jinja2! go templates! hcl!", that is merely a DSL, which is no longer a configuration file; it is effectively a program in a crude programming language, interpreted by an interpreter (the program loading the file).