My Manic Pixie Dream Programming Language

November 20, 2020 | 15 min. read


In the trades, the work being done often defines the tools that are used. Software engineering - being a kind of trade, despite how much its practitioners might be averse to the label - is no different. One of the most important skills in the industry is simply knowing what tool is right for the job at hand. In spite of this, however, it's nice to have one language that you know deeply and can use for miscellaneous tasks or tinkering with side projects where the main goal is just to have fun rather than develop bleeding edge technology.

I've used about a dozen languages across several paradigms so far (although I can only claim real competency with a few), and have yet to find one that fits like a glove. In this post I'll describe what my Dream Language would like like, what it would not look like, and some actual languages that have come close. Disclaimer: this isn't trying to be a thorough justification or critique of the features/languages presented here, but just a brief mention of why I like or dislike them.

Nice to Haves

Functional Primitives

The language that has probably most influenced how I think about programming is Haskell. Viewing programs as composed transformations of data rather than big stateful monoliths, in my opinions, leads to much more streamlined and transparent code. As such my dream language would have to include a few basic things that make this possible. First class functions would be a necessity, in addition to the usual map, fold, filter, etc. standard library functions to reduce boilerplate. Anonymous functions requiring as little syntax as possible would also be nice, similar to ES6's arrow functions or Haskell/Lisp lambdas.

Algebraic Data Types

I almost regret learning languages that feature algebraic data types, because using languages that don't have any similar features is almost painful. Being able to create product and sum types is incredibly powerful, and so much more elegant that other features. To illustrate, if I wanted to create a type for a binary tree with ADTs I could simply do:

type TreeNode = Node TreeNode TreeNode Value | Leaf

and with one line we've captured the recursive nature of the data structure, the possibility of null/empty tree nodes, and the type of value that the tree stores. ADTs are even more useful for error handling and type safety; the Maybe type in Haskell or Option type in Rust completely removes the the billion dollar mistake from the languages. The Either/Result types in Haskell and Rust respectively also make error handling mandatory and built into type signatures as opposed to exceptions or error codes that can be ignored or invisible until they break something.

Strong, Static Typing

This goes hand in hand with ADTs, but I'm firmly in the camp that static typing reduces entire classes of bugs and allows me to feel far more confident about the safety of my programs. Proponents of dynamic type claim that dynamic typing allows for more rapid typing since you don't really have to know the types of things until something breaks, but I disagree. Static typing doesn't always mean explicit typing (see C++'s auto or Haskell in general), and decent tooling and documentation will let you know the types of things at a glance anyway.

Garbage Collection

This probably has more to do with the domain in which I'm working than my actualy preferences, but I'd rather have the computer do all the work for me when it comes to memory management. I never work on any projects with real-time constraints like embedded software or game engines, so avoiding GC cycles for a few milliseconds of better performance is just not worth the extra effort of tracking mallocs or deducing lifetimes. This isn't a win-win tradeoff though, as sometimes garbage collection can be very silly indeed.

First-Class Package Management

I can't tell what I dislike more - nonexistant package management, or multiple competing package managers. I would much rather have an included package and dependency manager like cargo, pip, or gem built in than have the freedom to choose between have a dozen inferior implementations. Likewise, I just have better things to do with my time than manually wrangle every dependency of my project without a package manager. This is a good example of how competition is not good for products with network effects and often leads to monopoly. For example, if there are a number of incompatible telephone networks, the value I get from buying a phone inversely correlates with the number of competing telephone networks. Similarly, if there are ten different package managers with different communities writing documentation and fixing bugs, the value I get from that package manager is less than if the entire language's community is using that package manager and writing documentation and fixing its bugs.

Meta-Features

While not features of the language itself, there are a few things that would definitely be dealbreakers. As a web developer I'd enjoy a full-featured, batteries-included web framework like Django or Rails or Laravel. An active community willing to answer questions on StackOverflow or GitHub issues is also a plus, in addition to accessible and up-to-date documentation. A first-class tool like rustdoc that generates identical docs across packages is also super nice.

Nice to Not Haves

Object-Oriented Programming

This will probably be the most divisive opinion presented in this post. I don't have quite as radical an opinion as others on this topic - I don't think OOP is regressive or harmful or dangerous, it's just a different way to think about how to organize software. I do, however, think that drinking too much of the Object Oriented Kool Aid leads to an enormous amount of boilerplate and moving parts that are a nightmare to debug. Of course, drinking too much of the Functional Kool Aid can sometimes be a problem too. React Hooks, for example, really seem like a solution looking for a problem. Call me old-fashioned (meaning more than a year old when it comes to JS frameworks), but I'm more than happy to stick with class-based components.

In any CS101 class you'll be taught the basic class Dog extends Animal extends Organism example, and this is very intuitive and abstracts incredibly well. In the real world, however, I've simply never found myself needing to create any of the complex hierarchies that OOP espouses. In cases where polymorphic behavior is necessary, neat hierarchies never quite fit and traits/typeclasses on structs have been more than powerful enough to get the job done anyway.

Among the "hot" new languages of the past decade or so like Go, Rust, Elixir, and Clojure, most share a stark lack of traditional object-oriented programming features. I think this is a telling trend that the industry is tired of the dominance of the OOP paradigm, or at least willing to try something new in the decade to come.

Macro Systems

As a casual Lisp enthusiast, I can definitely appreciate homoiconicity and self-modifying code as a cool feature. A lot of the web frameworks I use similarly rely on macros for critical functionality. Ultimately though, I think that macros are more trouble than they're worth. At best, they encourage opaque black magic and the Lisp curse, and at worst you get the C preprocessor.

Significant Whitespace and Opinionated Formatting

These are semi-unrelated, but kind of similar in that I wish programming language designers would just leave me alone and let me have shitty (read: non-existent) style standards in peace. At the end of the day, it really shouldn't matter to the language whether I use tabs or spaces or how many spaces represent a tab or if I alphabetically sort my imports, and even being able to turn that off with line in a config somewhere is just one more thing to fiddle with instead of actually getting meaningful work done.

Go is probably the most egregious offender here. In no universe should an unused variable be a compile-time, build-failing error. I really don't care if I can just run gofmt or it's automated or whatever - again, this is just one more thing that I now have to include in my workflow that I'd rather not worry about in the first place. In terms of "consistency" across codebases, I really don't see how it should be the decision of language designers rather than individual organizations on what should be the default style or if there really needs to be one in the first place.

The Close Contenders

Rust

For a lot of the things I want, I've pointed out direct examples straight from Rust. Also, considering how much I enjoy Haskell and how many features of Rust are borrowed from Haskell, it seems like Rust would pretty much fit the bill. Unfortunately, this isn't really the case. While the small project or two I have done in Rust have been fun for the most part, I just don't find myself needing the features in Rust that happen to demand most of my attention. I'm never really working on anything safety- or memory-critical enough to justify the amount of time I spend fighting the borrow checker, nor am I ever low-level enough to care about the performance gains from zero-cost abstractions. I'm still a very big fan of the language in general and won't hesitate to use it when the situation calls for it, but for the reasons mentioned it's certainly not my Dream Language.

TypeScript

This might not necessarily count since TypeScript is technicaly just a type checker for JavaScript, but it seems fair enough to call it its own language. TS ticks all my boxes when it comes to typing - ADTs, strong typing, and first-class functions. TS also inherits the absolutely enormous JS ecosystem (for better and for worse), so there's more than enough of a community and popular libraries. The fatal flaw with TypeScript for me, though, is just getting a project up and running. Using TypeScript in any meaningful way introduces an entire toolchain and build system (Webpack, Babel, npm, etc.) that's hardly reproducible without using pre-written scripts like create-react-app, and God help you if something breaks. Again, I think TypeScript is a great tool and I use it anytime I'm near the frontend, but I'd rather not touch a webpack.config unless I have to.

Go

I really want to like Go for a number of reasons. It seemed like the best of C and Python; it's performant, lacking bloat or legacy features (you can probably learn it in the space of an evening or two, especially if you skip the advanced concurrency stuff), has pretty nice built-in tooling, and there's a huge amount of libraries if you ever need to step outside of the fantastic standard library. However, there's just a few too many things about it that prevent me from really buying into it.

As I mentioned earlier, opinionated formatting is incredibly obnoxious, and the language in general seems equally opinionated. The documentation and community especially seems to give off the vibe of, "This is the only way that you can do X, and you're being naive by even bringing up something else." Considering some of the opinions of its "The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt." (1), and "Syntax highlighting is juvenile." (2)
- Rob Pike, designer of Go
, I'm confident that this is not me being overly sensitive.

The language really seems like it was designed by a rogue AI that was originally opitmized for simplicity. So many things would be much more tolerable with just a tiny bit more added complexity. For example, error handling is little more than repeating

value, error := functionCall()
if error != nil {
    // handle error
}

several hundred times throughout a codebase. The missing functional toolbelt like map, fold, filter, etc. is also sorely missed and the dogmatic approach of using for for every situation where iteration is necessary also adds totally avoidable boilerplate. A lack of any kind of generics similarly creates unnecessary work, although I must admit that the interface system is usually enough to get the job done. A much more in-depth blog post about some of Go's sharp edges with regards to its fanatic approach to simplicity can be found here.

Conclusion

The logical thing to do after enumerating all the specific things I want and do not want in terms of language design would be to go about creating my utopian language. While I would like to be the change I want to see in the world, devoting what would likely turn into years of my life to a project with no guarantee of success just doesn't seem like a good allocation of my time. I'm more than happy to stand on the shoulders of people much smarter than me and grumble about minor language grievances while actually getting meaningful work done.

Considering I'm always trying to expand my knowledge and learn new languages and paradigms, this list will probably become little more than a snapshot of my current preferences in a year or two. There's a couple languages that seem promising that I might check out - Elixir and the Phoenix framework seem cool, and I've seen a lot of blog posts from the Nim team indicating some very impressive work on the language. I've never taken a deep dive into Prolog so maybe logic programming will be the second thing to revolutionize my views on software, or perhaps taking a trip to the source of OOP and learning Smalltalk might change my views on the object-oriented paradigm.

In the meantime, I'll just work on learning how to pick the right tool for the job. Or, at the very least, just pick what I get paid to use.