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 malloc
s 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.