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.
Twitter: @mattmight
Instagram: @mattmight
LinkedIn: matthewmight
Mastodon: @mattmight@mathstodon.xyz
Sub-reddit: /r/mattmight