26 programming languages in 25 days, Part 2: Reflections on language design

I recently wrote about completing Advent of Code 2022 using a different programming language (or two) every day for 25 days.

That note focused on the strategy, tactics and logistics involved in using 26 languages in 25 days without saying much about the languages or the experience itself.

Using so many languages in such a short span provided insight into tradeoffs in language design.

Here are my two high-level reflections from the experience:

Read on for more specific reflections on language design.

On syntax

Aside from notable (negative) experiences in bash, vimscript and TeX, language syntax mattered little to me.

I have a soft spot for S-Expressions, so I really enjoyed using Common Lisp and Racket (despite having never used Common Lisp previously).

Somewhat surprisngly, indentation-sensitivity in languages like Python and Haskell had me writing cleaner-looking and more readable code by default.

In fact, I routinely found that even in the languages where an end or an endfunction or } was needed, the indentation always predicted where it would have gone anyway.

Based on this experience alone, if I were to design a non-S-Expression-based language, it would push me toward indentation-sensitivity.

That’s a notable shift from my prior perspective, where I felt that whitespace-insensitivity was preferrable to indentation-sensitivity.

On static versus dynamic typing

I’ve bounced back and forth between statically typed and dynamically typed languages my whole career.

I’ve never settled dogmatically into any camp, and this experience didn’t change that.

On the contrary, I probably feel more firmly than ever before that neither philosophy is strictly superior, and that approaches than enable a mixture of static and dynamic typing are critical.

For instance, puzzles that emphasized manipulating complex nested data structures benefited from having the type-checker looking over my shoulder.

And, in general, I did feel that statically typed languages (especially the stricter ones like Haskell and Standard ML) reduced overall debugging, and more often than not, my code worked correctly once it compiled.

However, for puzzles where the data structures were straightforward, dynamically typed languages had me moving faster out of the gate.

In particular, days where dense matrices were the primary data structure seemed to favor dynamically typed languages.

If I were to design a language, I’d probably follow the approach of Racket and Typed Racket – of having a dynamically typed language as the default and a closely related typed language into which code could be gradually imported as needed.

On purity versus impurity

I used pure (or almost pure) functional programming languages (Erlang, Standard ML, Haskell) on a few days.

On some others, I programmed in a purely or almost purely functional manner even in languages that fluidly supported side effects (C#, JavaScript, Common Lisp, Scala, Racket).

In general, programming functionally tended to reduce the amount of time I had to spend to refactor my code to solve part 2, often significantly.

There also seemed to be far fewer bugs in my code.

On no day did purity seem to make a meaningful impact on performance.

As a programmer, my instinct will remain to use purely functional programming until there is a compelling reason to use side effects.

However, if I were to design a language, I probably wouldn’t remove mutability.

Rather, I’d encourage purely functional programming with rich, purely functional data structures in the standard library.

And, I’d provide strong up-front support for “transparent” side effects like memoization.

On lazy versus strict

Haskell was the only truly lazy language I used, but others like Scala and Racket had support for using laziness as needed.

To solve several of the puzzles, I rolled laziness by hand into the algorithm itself.

There were certainly days where that laziness improved performance substantially.

My sense, however, was that laziness by default was overkill, with laziness on demand sufficient in every case.

Were I to design a language, I’d probably opt for strict by default with substantial support for laziness as needed.

On compilation versus interpretation

It doesn’t make sense to think of a language itself as compiled or interpreted, even as language implementations tend to favor one approach or the other.

While compilation versus interpretation never made a sigificant difference in performance for my solutions (which tended to be dominated by algorithmic complexity concerns), there was a significant difference in the rate at which I learned the language.

Having access to an interpreter to poke at the run-time values of a partially completed program or to quickly test out an idea was a noticeable accelerant.

Were I to implement a language, I think I’d favor having an interpreter first.

That said, in language design, I’d hold back on features like eval that can substantially complicate compilation.

On domain-specific versus general-purpose

Not surprisingly, domain-specific languages (e.g. sed, awk, MATLAB) tended to be pleasant within their intended domain, and miserable outside of it.

A notable exception was vimscript.

It felt miserable to program in vimscript even when I was using it to write programs intended to manipulate text interactively.

What I’d really like to see is more support for embedded domain-specific languages within general-purpose languages.

Final thoughts

Of course, Advent of Code isn’t intended as a language design exercise.

Even so, the chance to kick the tires on so many languages in such a short span did shift my long-settled perspectives on language design.

Of existing languages, Racket (paired with Typed Racket) probably comes closest to what I feel is the sweet spot for language design, and it may explain why I seem to pick it more than most other languages when given a choice.