evan_tech

Previous Entry Share Next Entry
02:40 pm, 18 Nov 03

language cruft

One of the Perlers wrote somewhere about the Law of Conservation of Cruft. The idea is simple: if your program has some parts that are necessarily ugly, there are two ways to approach it. One is to keep your language pure, and make your program ugly; this is the approach exemplified in my mind by Python and Java. The other is to make your language ugly, the approach exemplified by Perl.

A good example of the difference is subgroups in a regular expression pattern match. Both Python and Perl can match a variable against a regular expression like /^(.)(.)$/. In Perl, this ($foo =~ /^(.)(.)$/) evaluates to something nonfalse¹, and stuffs the two subgroup matches into the magic variables $1 and $2. Ugly! In Python², this (re.match('^(.)(.)$', foo)) returns a MatchObject, which you then can extract the subgroups out of via obj.group(1). Verbose!

What's amusing to me is that Ruby supports both idioms: Perl-style, which still uses $1, $2 (which have weird magical behavior different from other Ruby variables) and Python-style (/(.)(.)/.match(foo) produces a Match object)... and in all the Ruby code I've ever seen I have never seen anybody use the second form.


These sorts of tradeoffs is so common in natural langauge that the idea has its own name (that I forget). A language like Japanese has only 8 or so consonants and 5 vowels³, so words are longer. But because a listener only has to distinguish between 5 vowels (contrast to English, which has somewhere around 12), speakers can speak significantly faster. The same tradeoffs arise across the entire spectrum of language:
  • German uses more complicated words to express complicated ideas in a single word, pushing the detail of what they mean to the speaker's lexicon. English borrows all sorts of meaningful, interesting words like zeitgeist and schadenfreude verbatim because we can't say them without using multiple pieces ("spirit of the times").
  • Arabic (only three vowels!) has such simple-looking characters that they skip vowels and have lots of diacritics. A nice thing about simple characters is that they can be much more expressive with it than we can. Check out the Arabeyes logo: that's actually legible!


In natural language, it's hard to say one design decision is better than another. (Some would argue that language naturally optimizes itself, so they're all near optimal.) Some aspects of programming languages are debatable, too, such as the above cruft balance. So where do you offload complexity? As with language, you push down in one place and it pops up in another. The simpler Perl-style rexep syntax produces magic variables. Strong typing allows you to find more errors at compile time while still compiling to fast code (think C++ templates versus Java collections), but you lose expressivity (in terms of dynamicism) and simplicity (writing out types and declarations everywhere).

I think the appeals to consistency and simplicity in programming langauge design are faulty goals. Languages like Chinese-- where (in a very real sense-- I just asked the native speaker sitting next to me) every syllable is its own word and you combine them to make more complicated concepts-- really appeal to the engineer in me, who likes the idea of building complicated things out of simple and powerful building blocks. But most languages aren't Chinese; we naturally gravitate to some (conceptually ugly) medium mix of atomic concepts and more complicated words. LISP and Smalltalk were appeals to the engineer's instinct, and while they're both useful to think about, I'll always choose a more moderate language.


Here are a number of constraints that I don't think are controversial:
  1. Programmer time is much more valuable than computer time, with one exception:
  2. Runtime programs (code) should be as fast as possible.
  3. Programs should be as terse as possible while still remaining clear: whenever you cut'n'paste you introduce potential errors and more code to read for a successor.
  4. As many errors as possible should be detected at compile time.


There's a place you can offload the hardness of these problems for free: the compiler. You write a fancy compiler once, and all future programs benefit. (Compiler complexity is of course tied to language complexity, because depending on how the language is designed the compiler needs to be more or less clever.) An optimizing compiler lets you write simpler code but still have speed advantages. With type inference, you can catch type errors at compile-time but you don't have to write them out. Sure, OCaml's type checker is black magic code that's even doubly-exponential (that's O(2^(n^n))) in some cases, but once it's written, you don't have to worry about it any more.



1 Actually, it looks like it's an array of the subgroup matches? I never knew this, though, and I suspect most of you didn't, either...
2 Feel free to fix my Python if it's not idiomatic; I used to be pretty comfortable with Python but I'd never use it for string-parsing tasks.
3 All statements like these about natural languages really depend on how you count, but rough numbers will do.