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.
Related articles
- 26 languages in 25 days: Strategy, tactics, logistics
- Tree transformations: Desugaring Scheme
- Lexical analysis in Racket
- Grammar: The language of languages (BNF, EBNF, ABNF)
- What is static program analysis?
- Implementing Java as a CESK machine, in Java
- Writing an interpreter, CESK-style
- Order theory for computer scientists
- HOWTO: Translate math into code
- Writing CEK-style interpreters in Haskell
- Closure conversion: How to compile lambda
- How to compile with continuations
- Understand exceptions by implementing them
- A-Normalization: Why and How
- Compiling up to the λ-calculus
- Parsing with derivatives (Yacc is dead: An update)
- By example: Continuation-passing style in JavaScript
- 7 lines of code, 3 minutes: Implement a programming language
- Architectures for interpreters
- First-class macros from meta-circular evaluators
- Programming with continuations by example
- Compiling Scheme to C
- Compiling to Java
- Church encodings in Scheme
- Non-termination without loops, iteration or recursion in Javascript
- Memoizing recursive functions in Javascript with the Y combinator
- Advanced programming languages
- Recommended books and papers for grad students
- Parsing BibTeX into S-Expressions, JSON, XML and BibTeX
- Low-level web programming in Racket
- Desugaring regular operations in context-free grammars
- Understanding and implementing laziness
- Higher-order list operations
- 99 ways to say '(I love you) in Racket
- Hacking the OS X Address Book with C, Racket and LaTeX
- Parsing S-Expressions in Scala
- 2011 Scheme Workshop: Accepting Submissions
- Lexical analysis and syntax-highlighting in JavaScript
- k-CFA: Analyzing types and control-flow in dynamic languages
- Fast vector-structs in Scheme from syntax-rules
- Matching regular expressions with derivatives
- Fermat and Solovay-Strassen primality tests in Scheme
- An implementation of RSA in Scheme
- Standalone lexers with lex: synopsis, examples, and pitfalls
- HOWTO: Change your behavior
- 12 resolutions for programmers [2023 update]
- HOWTO: Avoid temptation
- HOWTO: Control yourself
- Productivity tips for academics
- Travel hacks
- Tips for work-life balance
- 12 resolutions for grad students
- End artificial scarcities to boost productivity
- Boost productivity: Cripple your technology
- Console productivity hack: Exploiting task frequency
- Church encodings and the Y Combinator in Python
- Learn Perl by experiment
- A quick overview of programming with bash
- 3 shell scripts that can improve your writing
- Introducing QuickCheck: Number theory and red-black trees
- Deleting from Okasaki's red-black trees
- Self-inlining anonymous closures in C++
- Self-inlining anonymous functions in C++
- Lambda-style anonymous functions in C++
- Lambda-calculus in C++ templates
- Lazy-list-based streams in Scala
- An interpreter for Lambdo
- Okasaki red-black tree maps in Scala
- A pipelined, non-blocking, extensible web server in Scala
- Learning Scala in small bites
- Parsing M-Expressions in Scala with combinators
- Implementing regular expressions and NFAs in Java
Twitter: @mattmight
Instagram: @mattmight
LinkedIn: matthewmight
Mastodon: @mattmight@mathstodon.xyz
Sub-reddit: /r/mattmight