Vivian Voss

The Grammar We Rebuilt in Templates

it philosophy config languages kubernetes freebsd

Twelve files. Five hundred and thirty-nine lines. That is what helm create puts on disk when you ask it for the scaffolding of one ordinary web service, before anybody has written a line about what the service actually does. Render the thing and 100 lines of YAML come out the other end.

Open the files and the count changes shape. templates/deployment.yaml holds 78 lines, and across the scaffold 214 template directives sit between the braces, spread over nine of the twelve files. A single file therefore speaks three notations at once. YAML supplies the structure and Go templates the control flow, with a function library for everything those two cannot express, and anybody who has debugged an indentation error inside a conditional inside a loop knows the sensation.

What goes in, and what comes out source written by helm create 539 YAML rendered from it 100 The difference is the machinery: 214 template directives, a values file of 161 lines, and the scaffolding around them.

None of this is a complaint about Helm, which does what it was built to do. It is a question about what we are doing here at all, and to ask it properly I need to go back ninety years, to a room that had no linguists in it.

The inheritance

Every programming language you are likely to use was designed by a mathematician. Turing gave us the machine in 1936 and Church the lambda calculus in the same decade. Backus built FORTRAN in 1957, and the name says the whole of it: formula translation. McCarthy built Lisp on Church's calculus in 1958. Hoare brought formal verification and, by his own cheerful admission, the null reference. Dijkstra wanted programs to be proofs.

Not a linguist among them, and the metaphor they worked from was the equation. Variables bind. Types are sets. An error stops the program because in a proof an invalid step ends the argument. In 1936 that was the obvious and correct way to think, and we are still writing in their notation.

What natural language does, and what their notation does not, is worth naming precisely, because the rest of this piece is about where those three things went.

Words inflect. Run, runs, running: the form changes and carries the change in meaning with it. A function in code is a function is a function, and it never learns to conjugate.

Meaning depends on where you stand. "Here" means something different in every room, and every speaker in the room resolves it without being told. Code offers a handful of scoping boxes, most of them settled in the sixties.

Parts agree with each other. A verb agrees with its subject and an adjective with its noun, and nobody consults a central authority to make it happen. Types are checked, which is a different thing entirely: agreement is mutual, and checking is done to you by a program that was given the right to refuse.

Before the complaint goes any further it needs a boundary, because the obvious reading of it is wrong. This is not an argument against mathematics in software, and anybody making that argument should be kept away from the machines. Mathematics is what makes a compiler fast and a type checker sound. It is how a scheduler is proven to terminate, how a cryptographic routine is shown to do what it claims, how an optimiser rearranges a loop and can still demonstrate that the result computes the same thing, and how a borrow checker refuses a program that would have failed at four in the morning. Under the surface, where the machine lives, formal reasoning is the only instrument that works at all.

The surface is a different matter. The part a person reads and writes is communication, and communication had been studied for a century by people who were not in that room. Efficient functions, verified routines, a fast optimiser and a proof that the loop terminates are mathematics doing what it is superb at. Writing the notation a tired human reads at four in the morning was never that job.

Backus himself asked the question in his Turing Award lecture of 1977, under a title that is still the best thing anybody has written on the subject: can programming be liberated from the von Neumann style. Colin S. Gordon put the linguistic case properly in a paper at Onward! in 2024, and anybody who wants the academic version has it there.

What came back, and where

Here is the thesis, and it is not the one I expected to arrive at when I started counting files.

The three things the mathematicians left out did not stay out. Every one of them came back, a layer above the programming language, in the files where we describe systems. They came back badly, because the place they came back to is a data format with no semantics of its own and a macro processor bolted to the side.

Where the devices ended up API server validates the rendered result, never sees the source that produced it configuration layer: data format plus template engine inflection → templating contextual scope → a stack of overrides, four deep agreement → schemas, checked after the fact programming language inflection, contextual scope and agreement were never provided here

Inflection came back as templating. A deployment for staging and a deployment for production are the same noun in two cases, and since YAML cannot decline a noun, we generate both from a template and a values file. The declension is real, and it is performed by string substitution before anything has been parsed.

Contextual scope came back as a stack of overrides. A chart ships defaults, an environment file overrides them, a parent chart overrides that, and the command line overrides everything. That is context, expressed as precedence, and the only way to know what a value will be is to run the renderer and look.

Four sources, one value chart defaults environment file parent chart command line rendered value precedence decides To learn which source won, render the result and read it.

Agreement came back as schemas, and this is where the shape gets interesting. A Kubernetes manifest is validated by the API server, which knows precisely what it wants. The file that produced it was written in a format whose 1.1 specification says that the bare word NO is the boolean false, which is how a country code becomes an untruth in a configuration map. YAML 1.2 fixed that in 2009. The parsers in the pipeline mostly did not follow, and the one Kubernetes tooling uses still behaves closer to the old rules. The word arrives, the type changes on the way, and nothing reports an error, because from the reader's side a value that was silently converted looks exactly like a value that was meant.

I put that to the test on the chart from the top of this piece. A values file with region: NO under the node selector renders as region: false, in Helm 3.21.0, today. Put quotation marks round it and the country survives.

$ helm template demo -f values-a.yaml | grep -A2 nodeSelector
      nodeSelector:
        region: false

$ helm template demo -f values-b.yaml | grep -A2 nodeSelector
      nodeSelector:
        region: "NO"

Then there is the whitespace, which is where the arrangement turns from awkward to unsafe. Indentation carries meaning in YAML, and a templating engine writes indentation with spaces it has counted itself. I moved one key two spaces to the right in that same file, so that requests stopped being a sibling of limits and became its child. Helm rendered it without a murmur and produced a manifest that declares a limit called requests, which is not a thing that exists. The same file with a tab in place of the spaces fails at once, with found character that cannot start any token. One editor configured differently from the last one, and you have either a parse error or a different program, and the file looks fine in both cases.

Three linguistic features, rebuilt by engineers who were not thinking about linguistics at all, in the one place where an error has no grammar to catch it.

What the counting showed

I built the same small thing twice this morning, on the machine this column is written on. The Helm side is above: twelve files, 539 lines, 214 directives, three notations, 100 lines of output. The other side is a jail configuration on FreeBSD, where the same idea of a service in two environments comes to seventeen lines in one file, in one notation, with the difference between the environments expressed as two lines of override inside a block that inherits everything else.

exec.start = "/bin/sh /etc/rc";
exec.stop  = "/bin/sh /etc/rc.shutdown";
mount.devfs;
path = "/jails/$name";
host.hostname = "$name.example.com";

www_prod    { ip4.addr = "192.0.2.10"; }
www_staging { ip4.addr = "192.0.2.11"; devfs_ruleset = 5; }

That is contextual scope, done the way a language does it: the inner block inherits the outer one, the variable $name inflects each entry with its own identity, and there is no second notation anywhere, because the file is not generating text for another parser. The manual page that documents the whole format runs to 198 lines.

I am not claiming the two do the same job, and the comparison would be dishonest if I let that stand. Helm describes an application across a fleet of machines it has never seen; jail.conf describes services on one machine that the author can look at. What the comparison does show is what the extra ambition costs in grammar, and the answer is a factor of about thirty in source lines and two additional notations in every file.

The strongest objection

The case for templating is better than its critics allow, and it should be made in full.

Helm came out of a hackathon at Deis in 2015, went into the CNCF in 2018 and graduated in 2020, and it spread because it solved a problem that genuinely existed. Nobody can maintain a thousand hand-written manifests, repetition is more expensive than abstraction, and a package format that a stranger can install with one command is worth a great deal. The Go template engine was the pragmatic choice in 2015: it was in the standard library, every Kubernetes engineer already had it, and a new configuration language would have needed adoption that a young project could not command.

The serious answers to the format problem exist as well, and they are good. CUE, Dhall, Jsonnet and HCL give configuration types and composition, and none of them splices text to get there. They are chosen rarely, and the reason is not technical.

And the ecosystem has begun correcting itself, which deserves saying plainly because it cuts against my argument. KYAML, a stricter subset of YAML in which every string is quoted and every type is explicit, arrived as an alpha in Kubernetes 1.34, reached beta in 1.35, and became stable this August in 1.37. That is the platform conceding that the format could not say what it meant, and then doing the unglamorous work over three releases.

What a linguist would have built

So much for the complaint. The position underneath it is worth stating plainly, because it is not a plea for less formality.

The surface of a programming language is a human interface, and it should be designed by people who study how humans encode meaning. The devices are known and have been catalogued for a century: a form that changes to carry its own role, a scope resolved by where the reader stands, agreement between parts that needs no central authority, and compounds that build meaning by combination, the way a bookshelf is a shelf for books. None of that is vague. It is simply a body of knowledge that was never in the room when the notation was settled.

The few times somebody with that training did turn up, the result went further than the theory predicted. Larry Wall took a degree in natural and artificial languages and put more linguistics into Perl than the field thought sensible, arguing that a language should be optimised for expressive power because you learn it once and use it for years. Grace Hopper had COBOL written to read like English, and the thing is still moving money through the world's back offices sixty years later. Yukihiro Matsumoto designed Ruby around minimising his own surprise, which puts the comfort of the reader at the centre of the design, and Ruby went on to carry a generation of web applications.

The objection to all of this is ambiguity, and it is the serious one. Natural language tolerates several readings of the same sentence and relies on a listener to pick the right one, while a program has no listener and cannot afford the choice. Quite so. The ask here is for the devices, not for the tolerance: inflection and context and agreement are mechanisms for packing meaning into a compact form, and they can be given exact semantics by the same people who prove a type system sound. That is the division of labour the piece has been circling. The surface is designed by those who study how meaning is read, and the engine underneath is built and proved by those who are superb at proving things.

Ninety years on, nobody has commissioned that language. What we commissioned was a text substitution engine over a data format, and we call the result declarative.

The limit

Four things, and the first one is the one that stings.

The flat file does not scale by itself. Seventeen lines are seventeen lines because one person can see the whole machine. Put a hundred machines behind it and something has to generate those blocks, and at that moment the same question returns in the same shape, which is exactly why Helm exists.

The comparison is not like for like, as I said above and repeat here so that nobody has to catch me at it.

The grammar I am asking for may not be possible without a language, and a configuration language is a language, with everything that follows: a specification, an implementation, a community, and twenty years of carrying it. The projects that tried are still small, and calling that a failure of taste rather than a failure of effort would be unfair to people who did the work.

And I am not neutral here. I have spent a good deal of my own time on the format question, which is a reason to test what I write rather than to believe it.

The point

Backus asked whether programming could be liberated from a style that had been chosen for the machines of the forties, and the answer we have given, half a century later, is that we would rather not. We kept the notation. Then we took the parts of human language it lacked, the parts that belong in a language with a grammar and a specification, and rebuilt them out of string substitution in a file format that cannot tell a country from a falsehood.

The alternative is an ordinary arrangement: a surface drawn up by people who study how meaning is read, sitting on an engine proved by people who are superb at proofs, which is roughly how every other engineering discipline divides its labour.

Ninety years of writing in the mathematicians' notation, and the one thing we added ourselves was a macro processor.

Twelve files and 539 lines for one service, rendering to 100. Inflection came back as templating, contextual scope as a stack of overrides four deep, and agreement as schemas over a format in which the bare word NO parses as false. The same service in two environments is seventeen lines of jail configuration in one notation. The devices belong in the language, designed by people who study how meaning is read, with the proofs kept underneath where they work.