The programming language and tooling I want

I like programming languages and tooling. But I think we can do a lot better than what we have now.

Here are some very rough thoughts on things that my ideal programming language tooling setup would have. Some of it would probably require person-decades of work to achieve. Probably some of what I want is in direct conflict with other things I want. Sorry about that.

No text files

Notable prior reading: Unison.

The code repository should be stored and checked in to source control in an abstract semi-compiled form. To edit code, you would search through the repository using a CLI or GUI tool, then select the function or module you want to edit. This would open a text file in a regular editor. Then to save your edits, you’d save the text file and use the tool to update the canonical stored definition from your text file. The tool would ensure your code type-checks, then compile it down to some abstract intermediate form (like an AST) and save that.

The semi-compiled form would amenable to source control, either through things like a git filter or (more likely) just being human readable itself, but not meant for human editing.

Benefits of this approach:

Type system

Effects

Notable prior reading: this post.

Everything that may cause a side effect should be marked as such. Maybe via an “effects system”. But maybe instead of introducing a whole “effects system” with new and different rules, we can model effects by passing regular function arguments that take the parts of the “real world” that we need to access.

The program entry point would be passed an entire “real world”, and then you can pass down bits of the “real world” to functions that may need it.

It should be possible to look at a function signature and know: what side effects can it have? This would require us to answer: what are side effects? Should probably be at least:

Effects should be interfaces when appropriate, so you can “mock” the real world by passing a mocked type that e.g. provides randomness with a constant return 4, or a network call type that always returns HTTP 200 OK. You can’t really mock terminating the program though.

With this system, if we see a function that takes nothing from the “real world”, we can be sure it’s only doing very basic things like just reading/writing from CPU registers, doing integer arithmetic, branching, etc.

This is why the auto-refactor tools are important. People would get annoyed if every time they needed to add an effect to a deeply-nested function, it then meant they had to manually update hundreds of function signatures and call sites to thread through the effect-giving value. That rote work should be done for them automatically. Computers should serve humans, not the other way around.

Care needs to be taken around higher-order functions/closures. Should we require functions that takes closures to explicitly pass the effects to the closures? For example, if we have a function F that takes a function G of e.g. type int -> int, can G close over some effect-giving values? Then when we call G from the body of F, we’re actually accessing an effect, even though that didn’t show up in the type of G? We should maintain the property that we know exactly what effects can be accessed at any given time.

Concurrency

Notable prior reading: this post.

There should be no unbounded “spawn thread” or “spawn process” API. As the post explains, this is akin to goto.

Put another way, you must always be required to join thread/process handles. This should be known to the compiler, both in that the compiler should know you have to join, and the compiler should know that since you have to join other invariants are known and thus other potential transformations are available.

Something like Loom where you can see/prove all possible parallel executions would be cool too.

Cross-network-boundary compiler

Notable prior reading: React Server Components, a bit of Temporal.

You should be able to write code on the frontend and backend, or between services, and have it be well typed and resilient to network failures. The compiler should see the full picture of what cross-service calls exist, require to you pass well-typed arguments, and generate auto-retry code when appropriate, and also force you to handle the realities of distributed systems, where any node or service can degrade at any time.

Real-world resources known to the compiler

Notable prior reading: Dark, Wing.

This is getting a bit more into areas I know less about, and also kind of far from what a usual programming language is.

With that said: I really don’t like how currently we have “regular code” in “proper” programming languages, but then when it’s time for Kubernetes resources or DNS rules or network access control lists or whatever, everyone breaks out the YAML and the hardcoded config strings and the going into the cloud console to fiddle around with buttons and UIs.

Everything should be fed into a big “compiler” which then understands what actual resources you need, both in the sense of on-node resources like threads, memory, and disk space, and distributed resources like IP allowlists, DNS rules, etc. The compiler should know what services are deployed where, how they talk to each other, what nodes need to exist, etc etc, and arrange for that to happen.

I’ve heard some of the BigCos have something close to this technology, but they don’t open-source it.

Other miscellaneous things

Closing thoughts

We can do better than LLMs slinging around thousands of lines of instant tech debt AI slop in current programming languages. The fact that we feel the need to do so shows that the abstractions we’ve built aren’t abstract enough.

Imagine if LLMs arrived before “Go To Statement Considered Harmful”, one of Dijkstra’s many important works and the original “considered harmful” paper, and we still wrote programs with gotos. Or if LLMs arrived even before that, when all we had was assembly.

We’d probably say exactly what we say today, which is: Look! We can generate thousands of lines of code really easily and quickly according to common patterns! This will increase productivity!

Except the maintainability of this mess would be a nightmare, because it’d be totally intractable for humans to gain a good mental model of it.

I argue that we’re essentially in that state right now, with our current set of programming languages and LLMs. It’s a bit better because current languages are indeed more higher level than gotos and assembly, but it could be way better, with better languages and tooling.

Programming is theory building. Our languages and tooling should let us humans focus on building the theory and do for us the rote, repetitive work to make the theory a reality.