Formatting is an interface between code and reasoning

Formatting is an interface between code and reasoning

Code formatting has a bad reputation for a good reason.

A lot of formatting arguments are boring. Tabs or spaces. Where the comma goes. Whether a record should be vertically aligned. Whether a formatter is "ruining" someone's carefully arranged code. These "bikeshedding" conversations can soak up surprising amounts of energy without changing what the program does.

So the natural reaction is to say: stop talking about it. Pick a formatter. Move on. That reaction is basically right. Formatting should not be a recurring argument. But it does not follow that formatting is trivial.

At Obsidian, we have a Haskell style guide and a small tool called style.hs. It wraps Fourmolu with the formatting configuration we use across our Haskell projects. You can run it with Nix, use it locally, and put it in CI. The point is not to make formatting a centerpiece of engineering life. The point is to make it disappear into the background, which is exactly where a lot of important infrastructure belongs. We have a similar formatter in development for Daml — closed source today, but on its way to an open source release.

A formatter does not prove your program correct. It does not make a poor abstraction good. It does not save you from bad names, leaky abstractions, or confused domain models. But it changes the surface through which people encounter the code. And, even in the age of LLMs, how people interact with code, how legible it is to humans, matters a great deal.

Formatting isn't just about aesthetics. It is an interface layer between computer code and human reasoning. It makes code more inspectable. Humans get visual structure; LLMs get more regular token structure. In both cases, the result is fewer mistakes, better reviews, better patches, and less wasted effort reconstructing the shape of the program before thinking about what it actually does.

The shape of the program

Before you can reason about code, you have to understand its shape. That sounds obvious, but it is easy to forget how much work happens before "understanding" begins. You scan a file. You separate definitions. You notice a branch. You pair a type signature with an implementation. You decide whether a line is a continuation of the thought above it or the start of another one. You figure out whether a diff changed behavior or merely moved text around.

All of this happens more quickly when the code is laid out well. It is still work, but bad formatting makes that work more expensive.

Back in 2008, the author of Sublime Text realized this and added a code "minimap" feature to his editor. Like many great ideas, it was an obvious one in retrospect, and the feature has since made its way into countless editors and provides a bird's-eye view of the shape of the code and the locations of executions, errors, and warnings.

Good formatting makes the structure of a program visible before the meaning is fully understood. The reader can see that this is a case split, that this is a record, that this is a local helper, that these arguments belong together, that this change is isolated.

Consider a small Haskell example:

handleCommand env command =
  case command of
    CreateOrder order ->
      createOrder env order

    CancelOrder orderId ->
      cancelOrder env orderId

    ListOrders accountId ->
      listOrders env accountId

There is nothing clever here, which is part of the point. The formatting is doing ordinary work. It lets the reader see the cases before reading the details. It makes the repetition available to the eye.

Now compress the same idea:

handleCommand env command = case command of CreateOrder order -> createOrder env order; CancelOrder orderId -> cancelOrder env orderId; ListOrders accountId -> listOrders env accountId

Yes, the compiler can deal with it. Maybe an experienced Haskell programmer can deal with it, though with effort. The reader has to recover structure that the first version made plain. That recovery cost is small in one example but it scales with your codebase.

Formatting preserves attention

Our most important reason for formatting is not beauty (though we like that, too), it is attention.

Engineering work is full of attention leaks. Some are avoidable and some are not. Domains can be quite complex, every production system has a history, requirements can and do change, libraries can do the unexpected, bugs arise under pressure.

Given all this, formatting should not be another attention leak. The reader should not have to wonder whether two nearby expressions are aligned because they are conceptually parallel or because someone liked how it looked. The reviewer should not have to inspect a twenty-line diff to find the one line that changed. The person debugging late at night should not have to visually parse a wall of code before finding the branch that matters.

A shared formatter is a small act of kindness toward everyone who has to read the code later. This is why consistency is more important than personal preference. A consistent style that is slightly different from what one would choose for themselves is generally better than a codebase where every file reflects the taste of whoever touched it last.

There is a kind of freedom in abiding by the constraints of a chosen style, in not deciding. Once the formatter is chosen, the argument is over, the code gets formatted, the reviewer moves on. Conveniently, editor tooling can do the formatting and CI can enforce it. The team can spend its attention on the program itself.

This is the same reason we like other forms of automation that remove low-value judgment from the development loop. Not because judgment is bad but because judgment is scarce, and we want to use it where it matters.

Diffs are part of the program's interface

A diff is a record of changes to a codebase and also how code is reviewed. The quality of diffs, therefore, is an engineering concern.

If a small semantic edit creates a large textual diff, the review gets worse. The reviewer now has two jobs: understand the intended change, and filter out the accidental movement. That second job should be as small as possible (there's the attention budget again).

Some formatting styles look good in a static file but behave badly under change. Visual alignment is the classic example:

data BuildPlan = BuildPlan
  { buildPlan_source    :: SourceTree
  , buildPlan_toolchain :: Toolchain
  , buildPlan_output    :: Artifact
  }

This has a certain appeal. The fields line up and the human eye likes it. But once you add a longer field name lines that did not change may move. The diff is noisier than the semantic change warrants. The format optimized for looking at the file once, not for editing it repeatedly.

A less decorative style is often better:

data BuildPlan = BuildPlan
  { buildPlan_source :: SourceTree
  , buildPlan_toolchain :: Toolchain
  , buildPlan_output :: Artifact
  }

It is plainer. It is more stable. A new field is more likely to be a new line and nothing else. A codebase is not, after all, a screenshot. It is an object under revision and should be formatted for change.

Large codebases are maintained through a long sequence of small acts of reading and change. Anything that makes those acts cleaner yields a compounding benefit.

Why this matters in Haskell and Daml

Languages like Daml and Haskell give us expressive power. That is one of the reasons we use them. Types, purity, algebraic data types, higher-order functions, typeclasses, and abstraction let us say more with less. They let us move more of the reasoning into the program itself. Done well, they let a small amount of code describe a large amount of behavior while maintaining precision and without becoming vague.

But expressive power can cut both ways.

Dense code can be precise, but it can also be hard to get into. A good abstraction compresses detail in a way that helps the reader. A bad abstraction compresses detail in a way that hides the problem. The difference is not solved by formatting, but formatting affects how we encounter the code.

This is especially true in high bandwidth languages like Haskell and Daml, where a line of code can carry a lot: constraints, effects, pattern matches, operators, local bindings, point-free composition, and domain-specific vocabulary. The language rewards precision, but precision still has to be read by a person.

The point of formatting is to give that precision a stable form. It specifies the boundaries, the way things are nested, the repeated structures. It makes it possible to have expectations about what the code looks like and to pattern match on what you see. And it makes it easier to find the thing that actually changed.

The type system helps us reason about what the program can do while formatting helps us approach the program in the first place. Those are different jobs, but they belong to the same project: making software more available to human reason.

LLMs are readers too

There is another reader in the room now, of course. LLMs do not read code the way people do. They do not literally scan indentation with eyes. They do not experience a messy diff as annoyance. Still, they are very sensitive to structure. They work from patterns, locality, repetition, names, and boundaries. Formatting is good for them, too: well-formatted code gives them better material.

A consistent codebase gives an LLM more examples of the local idiom and a clean diff gives it a clearer signal about what changed, just as it does for a human reader. Regular formatting makes it easier for the model to keep related pieces of code together and avoid inventing a style that does not belong in the repository.

This does not, unfortunately, make LLM output safe, but it does make it easier to inspect for human reviewers and auditors.

The right response to AI-assisted programming is not to care less about structure because the machine can "figure it out." The machine often guesses, and often it guesses well. Sometimes it guesses in a way that looks good until a person stops paying attention.

Inconsistency makes that failure mode worse. When local idioms vary across a codebase, the model tends to flatten them into something plausible. The result reads correctly until someone notices that a distinction that mattered has been erased.

Given the imbalance in how much LLMs can produce compared to what humans can consume, it's important that we make the work more inspectable, not less.

When LLMs are participating in the development loop, they should encounter the same kind of code we want humans to encounter: regular, local, reviewable, interesting where attention is needed, and boring where it is not.

More compute does not buy understanding

There is a larger issue underneath all of this: software has always been a negotiation between what machines can execute and what people can understand. More computing power changes that equation, but not always in the ways we want.

More compute lets us build larger systems. It lets us generate more code, add more dependencies, process more data, and support more states. It lets us move faster. It also lets us create systems whose behavior outruns our ability to reason about them.

The machine can execute the system, but that does not mean we understand it. This is one of the old lessons of software engineering. Computing power does not dissolve complexity. Often it gives complexity somewhere to hide.

The major cause of the software crisis is that the machines have become several orders of magnitude more powerful! To put it quite bluntly: as long as there were no machines, programming was no problem at all; when we had a few weak computers, programming became a mild problem, and now we have gigantic computers, programming has become an equally gigantic problem.
-- Edsger Dijkstra, The Humble Programmer (EWD340), Communications of the ACM

At Obsidian, many of our technology choices are responses to that problem.

We use Haskell because we want more of our reasoning reflected in the language. We use Daml because multi-party workflows need a model precise enough to survive contact with real institutions. We use Nix because build inputs and environments should be explicit. We care about reproducibility because the path from source to artifact should be inspectable.

Formatting is much smaller than any of those things, but it's part of the same effort. It is a small way of saying: the human reader matters, review matters, quality matters, and the path from text to understanding matters.

The program is not only something for the compiler to accept and build, it is something for people to accept and maintain.

Haskell: What style.hs is for

style.hs exists to make this easier for our Haskell projects.

It gives us a shared formatting configuration. It can be run directly with Nix or it can be used in CI. For us, it keeps formatter behavior from drifting across repositories.

A typical local invocation looks like this:

nix run github:obsidiansystems/style.hs -- --mode inplace src/

A CI check can use:

nix run github:obsidiansystems/style.hs -- --mode check src/

The details are intentionally modest. This is not a grand framework (nor should it be!). It is a small piece of shared development infrastructure, and that is the right scale for the problem.

Formatting should be easy to apply, easy to check, and easy to stop thinking about. The style guide can explain the judgment behind the rules, but the formatter should handle the mechanical part.

Daml: A formatter in development

We have a similar tool in flight for Daml. It is currently closed source (we use it internally and with clients today) and we plan to release it under an open source license very soon.

The approach is the same as style.hs: a shared configuration, runnable from Nix, usable in CI, integrated with editors, and designed to disappear into the background.

The stakes are high in Daml. A Daml file often describes rights, obligations, authorization, and multi-party workflows. It may be read by engineers, product people, auditors, integration partners, and institutions with limited patience (and little legal/compliance room) for "trust us, the code does the right thing." Layout there is one of the ways a person sees the model and builds trust in it.

The work after formatting

After formatting, the real work remains.

You still have to choose good names. You still have to model the domain. You still have to decide where the abstraction belongs. You still have to test the behavior that matters. You still have to review the patch carefully. Formatting does not answer those questions. But it does make it easier to focus our limited attention budget on them.

And, really, that is enough. A lot of engineering discipline works like that. It doesn't produce one dramatic moment where the system becomes correct. Instead, it removes one source of confusion, then another, then another, so that people have a better chance of seeing what they are doing.

A well-formatted codebase is easier to read. A stable diff is easier to review. A consistent style is easier for humans and tools to follow. This is a win for reasoning, and aesthetics are along for the ride. And, in software, reasoning is the scarce resource.

We can produce code faster than ever. We can ask machines to generate, translate, summarize, and edit it. That makes the old question harder to answer, not easier: who understands the system, and how do they arrive at that understanding?

Formatting will not answer that by itself but it is one small place where we can choose inspectability over noise. To us, that seems worth doing.