Monday, February 26, 2007

Imperfect Approximation of Perfect Code

It needs to be said: the quality of a programming language is not inversely proportional to the number of bytes needed for a Sudoku solver. Not that there's anything wrong with comparing Sudoku solvers; they can contrast certain aspects of languages, and differing solution styles can help readers think differently about code. But this only gets us so far.

The problem is the public language debate rarely ranges to topics that span more than a couple dozen lines of code. Anything bigger is difficult to manage in a single discussion. Therefore we need a model to debate the higher-order concerns of programming languages. Fortunately, I'm shameless, and will shamelessly steal such a model from Donald Norman's excellent Design of Everyday Things.

The idea I'm referring to is Norman's modeling of human interaction with software and physical tools as a series of imperfect approximations to approach a perfect goal. A system should assume all user input is imperfect, but guide the user to her goal. A user should have feedback indicating progress toward the goal. Mistakes should be easily reversible and never take her far astray.

Let's apply this to programming languages and practices. The biggest step forward in recent years has been the adoption of test-driven development. This fits nicely -- a solid safety net of unit tests allows developers to move towards the perfect goal. It also brings to mind a humorous recent post by Jt Gleason. One of the reasons programming is harder than bridge building is even small defects can cause us to jump radically away from our goal.

So, what can improved languages and practices do to help? They can guarantee no programmer mistake will cause us to jump to across an ocean, to use Gleason's metaphor. Programming will never be truly analogous to bridge building, but if bugs can be localized -- like how a bad bolt is localized for a bridge -- we might make progress. We can't eliminate bugs, but we can bound them and trend towards perfection. This reminds me of a Piet Hein poem reportedly displayed in Donald Knuth's home:
The road to wisdom? Well, it's plain
And simple to express:
Err
and err
and err again
but less
and less
and less.
With all this in mind, I recently saw something that just might be a glimpse of the future. Giles Bowkett posted a screencast showing modification of code in a Seaside-based web application as it was running. Sure, we've been able to hot-swap code for a while, but that's usually a painful process requiring specific launch configuration and use of an external debugger. Imagine if every menu in every program had an "edit" button available to experts, allowing them to adjust code on the fly. The code-build-test-debug loop would become instantaneous, applications could be grown in the context of existing pieces, and good design and languages can prevent local bugs from creeping elsewhere. This is our series of approximations toward a perfect goal.

Of course, the above ideal might be unachievable for many applications. Even so, it still might be possible to build an imperfect approximation.

Sunday, February 11, 2007

Part 4: The Killer App

Technologies get adopted when there is a compelling reason. People bought PCs for the killer app; game consoles sold for the killer game. Programming languages take off because they have a killer feature. Philip Wadler pointed this out nine years ago:
Instead, experience shows that users will be drawn to a language if it lets them conveniently do something that otherwise is difficult to achieve. Like other new technologies, functional languages must seek their killer app.
Java was adopted largely because it was easy for C++ developers to pick up, and offered killer features like cross-platform development and garbage collection. So what is the killer feature of the next language? I'll take a shot: the complete and final eradication of undefined behavior.

No Behavior Left Undefined
"Undefined behavior" is the biggest euphemism in software engineering. It even beats out "non-trivial problem". It's why your code works great on your laptop, but every one of your users runs a DeathStation 9000. Amazingly, most mainstream languages still have areas where behavior is undefined and unpredictable.

Fortunately, modern languages have made great strides as compared to, say, C++. However, we still have a long way to go. The big problem these days is race conditions caused by improperly synchronized code. And it looks like concurrent programming will only become more common.

What makes undefined behavior particularly evil is not that it might fail; it's that it might succeed. We want incorrect code to consistently fail. This way every time we find a bug we can isolate it with a unit test and ensure it never happens again. But undefined behavior shoots a hole in this: we can have incorrect code that passes every possible unit test. The result is bugs showing up in the worst possible place, production.

Consider the practical implications of working in a language without undefined behavior. If we can create a reliable unit test for any bug we find, we can guarantee that bug never recurs. Good development practices can cause defect counts to trend towards zero. It also makes development itself easier by strengthening our firewall against complexity. Although we'll never eliminate buggy code, hopefully we can create enough stability to prevent bugs from spiraling out of control, which has led to the death of many projects.

Where is our new language?

The first three posts and this one describe an ideal for a new programming language. Many details were intentionally left out to focus on the essence, so many language styles might work. Now I have a question for you: what current or upcoming language best meets these ideals? Much of this exists in part in Haskell, Erlang, Ruby, Scala, F# or others, but I know of no language meeting them all. I hope that will change soon.

Thanks to everyone who made it through these posts. I do want to hear your thoughts.

Thursday, February 8, 2007

Part 3: Building a Firewall Against Complexity

In the first two posts in this sequence, I argued a few key points:
  • A language must be usable by mediocre developers to achieve widespread use.
  • A language that prevents quick and dirty application development will not be widely adopted.
  • Widespread adoption is important so good programmers can use better languages in their day jobs.
  • Haskell shows important innovation in language design, but has major hurdles to widespread adoption.
Assuming the above, we have a big problem: how do we make the essential innovation in Haskell available to the broader industry? We need to first define the essence of Haskell's contribution, and then look for ways to package that essence for widespread use.

I will use a simple model of software problems to define Haskell's essential contribution. Complexity kills, but Haskell helps contain it. We were in trouble the moment systems got too big for a single human to understand, and we kept digging the hole deeper. This needs to stop.

So far the best answer to complexity has been good design abstraction. And there have been successes -- I usually don't think about the virtual memory system when writing code, for instance. But as Joel Spolsky pointed out, "All non-trivial abstractions, to some degree, are leaky." I don't usually think about virtual memory, but if I have locality of reference problems and huge numbers of page faults, the abstraction leaks. Suddenly I have to think about everything my program does, everything its libraries do, all existing abstraction leaks, and the virtual memory system. Soon there isn't room in my limited mind to keep track of everything. The result is almost inevitable: a system gets more complex, details are missed, and defects creep in.

To make matters worse, the difficulty of understanding a system grows exponentially with size. Every variable added must be tracked with every other variable. I see two possible solutions to this:
  1. Stop building such complex software.
  2. Find ways to build airtight firewalls around complexity. Stop the leaks.
History suggests option one is not likely. Great programmers can solve complex problems with simple code, but this is not the broader trend. Even if it was, this answer is unsatisfying; it's really just avoiding the problem.

The second solution might have potential. Imagine if we had abstractions that didn't leak, even if they could only be used in special conditions, or applied to certain problems. There will always be complexity, but by bounding it with airtight abstractions we limit the number of variables involved. Heck, if we do a great job, it's possible even we humans will be able to understand the systems we build.

This is where we reach the essential contribution of Haskell (or any pure functional language): it builds a firewall around the rest of the system's complexity. You only have to think about the part you're working, and you can make that small enough to be understandable. Because functions can't have side effects, you don't have to worry about locking a resource or dealing with inconsistent state. Your universe is defined by a function's arguments.

So far, so good...but we still need to package our firewall against complexity to be more easily adopted and used. Notice large parts of functional languages are not included in the essential contribution. For instance, a chunk of code could be written in any style, as long as its side effects are encapsulated. Here we break away from the pure functional model. If local destructive updates, statements, for loops, or other imperative constructs make coding easier, we can allow them. The key is not allowing these local decisions escape; our firewall against complexity must hold. A similar point came up in a recent discussion on Channel 9. Erik Meijer described it as "local impurity, but global purity".

This is a key element for our hypothetical new language: allow developers to code in their style of choice, provided it does not compromise the system's guarantees. The merits of different styles are open to debate, but surely no one style is the best for all programs. Ultimately, our hypothetical language might really be a set of languages, all keeping the same guarantee of isolation. The key is we can build a language with stronger guarantees than any commonly used language, but make it easy for most developers to adopt.

Can we live without state?
System state is the biggest source of complexity for imperative programs. It's also the biggest reason imperative code is considered easier for quick and dirty solutions. You want to change state? Go for it. Just be sure you synchronize with other threads, and your state change is consistent with the rest of the system, and you leave the system in a consistent state in case of error, and...well, hope others changing the state follow the same rules.

All of these problems go away with functional languages. But I can't shake the idea that a language restricting all access to state simply won't get adopted. Haskell elegantly solves this solution with the Monad; you write code in isolation, and simply describe the side effects to be evaluated as needed. Sounds good, until you run into problems like the one described here. In that case, the developer discovered a deep function needed some additional piece of information, and had to refactor large amounts of code to get it there. This drawback may well be justified by the advantages of isolated coding. Then again, our goal is to get a better language adopted, and most businesses will reject one that makes writing quick and dirty code harder, even if there is a net gain.

I really hope I'm wrong about this one. (And I'm sure many will try to convince me that I am.) I suspect, though, that languages must make accessing state information easy to gain widespread adoption. Is there a way we can do this while holding on to our firewall against complexity? Maybe. There are some tempting ideas that might solve this. Possibilities include Software Transactional Memory (STM), or messaging-based models like Erlang. The answer to which is better must come from experimentation, although STM elegantly solves some problems others do not, as Tim Sweeny described (pdf). Assuming an adopted system must be able to easily change state, we can still use STM to attain many advantages of the functional style:
  • No race conditions or deadlock. STM can make code seem like it's running in a single-threaded environment. This is our firewall against concurrency complexity.
  • No inconsistent state. If any error occurs, the transaction is simply rolled back. As a result, the state space of the system is dramatically reduced, and all possible states are logically consistent.
  • External side effects (like database updates) should happen in the same transaction as memory updates, again ensuring consistent state.
A new language could allow only special annotated functions to use STM, clearly indicating which functions are logically pure. This is analogous to implicitly passing a Haskell Monad to every pertinent function. Yes, we are sacrificing functional purity. However, we keep our firewall, and build a language that just might find a widespread audience. Also, we need not sacrifice other great features of functional languages to get there. For instance, a function may do destructive updates internally but always return an immutable object, lending itself to memoization.

In summary, we have two new requirements for our hypothetical language:
Developers should be able to choose their style of programming, but still have strong guarantees of isolation from the rest of the system.
The system should limit the number of variables the developer must track while writing code.
In my next post, I will add more requirements in this spirit, and hopefully draw these thoughts to a close.

Monday, February 5, 2007

Part 2: Languages and the Lesser-Skilled Developer

My last post ended with an assertion: we need a language with the best of Haskell in a more accessible form. This leads to an old debate. After all, building good software requires tremendous skill. How can we make progress if we tailor it to the weakest practitioners? Bjarne Stroustrup made this point well:
The idea of programming as a semiskilled task, practiced by people with a few months' training, is dangerous. We wouldn't tolerate plumbers or accountants that poorly educated. We don't have as an aim that architecture (of buildings) and engineering (of bridges and trains) should become more accessible to people with progressively less training. Indeed, one serious problem is that currently, too many software developers are undereducated and undertrained.
Stroupstrup is right, of course. Too many developers are undereducated; we should demand greater skill. However, this doesn't change the fact that language adoption for a given shop has as much to do with politics as technical merit. Businesses have a perceived need to hire armies of developers to quickly write bad code and bad software. We'll be asking "WTF?" for a long time to come.

The problem is highly skilled developers are often forced to use the languages accessible to their less competent colleagues. Browse the forums at Reddit and you'll find lots of developers coding C# or Java by day and Haskell or Lisp by night. In short, a language must be usable by lesser-skilled developers so high-skilled developers have more opportunities to use it. Ruby is a great example of this in action -- building a quick and dirty web application is trivial, but the language still offers powerful constructs such as closures.

Battling the Hack
Anyone who has written software for a couple years has come across the Nasty Hack -- what should have been a simple piece of code made horribly complex, requiring an alignment of planets to work. Languages such as Haskell have the laudable goal of preventing nasty hacks by restricting what can be done in a given context. Sadly, good design often takes second place to deadlines -- and a language that has the appearance of slowing development won't get far, even if it offers a net gain.

Even a language that prevents some classes of nasty hacks will allow others. No language will free us from bad code altogether. Even worse, languages that free us from large classes of bad code have trouble getting adopted because businesses indirectly want to hack. This has depressing implications: the nasty hack is here to stay, and languages attempting to eliminate it will struggle to find widespread adoption. Our only hope is a fundamental cultural change in the way most organizations build software.

There is nothing I want to see more than this cultural shift. Maybe some day the market will get so sick of bad software it will force change. Consumers might realize great software is possible, as shown by a few innovative companies. Other companies might finally delay entry to a market, knowing a buggy solution will be rejected. The lesser-skilled developer might be forced to improve or leave the field.

However, while attempting to change the culture we still need to innovate under the current constraints. This means it must be easy to write quick and dirty code in a widely adopted language. So I submit a requirement for our new language:
A language should encourage good code -- but make quick and dirty code possible.
Today languages like Ruby and Python are probably the closest to the mark. Inexperienced developers can quickly get code working, and more advanced developers have access to powerful features drawn from the functional world. However, both of these languages have shortcomings when compared to Haskell. Incorrect code can lead to unpredictable behavior, and poorly designed code can lead to a state space too big for any human to understand. Specifically, they do not offer a tractable solution to concurrency and composability (pdf).

In my next post I will attempt to outline a language concept drawing from the best of Ruby, Haskell, and other sources.

(Edit: fix typo)

Saturday, February 3, 2007

Part 1: Programming and the Metaphorical Mind

Hello, World
This is the first of a set of posts on what we need from a new programming language. Everything I discuss exists in some language -- at least in part -- but no language yet has them all. This content is a mixture of my experience and many borrowed ideas, which I will do my best to cite. My goal is to represent the language needs of some segment of developers.

The Metaphorical Mind
I'll start simply: a language must be designed and taught with its users in mind. There is a psychology to programming which may explain why some languages get adopted and better ones don't. To model this psychology, I borrow the idea of the "Metaphorical Mind", as described in Steven Pinker's excellent How the Mind Works. In Pinker's words, "The human mind, we see, is not equipped with an evolutionarily frivolous faculty for doing Western science, mathematics, chess, or other diversions." Our minds evolved to work with the physical world, and we reason with abstract ideas using physical metaphors. Pinker cites Ray Jackendoff for examples:

The messenger went from Paris to Istanbul.
The inheritance finally went to Fred.
The light went from green to red.
The meeting went from 3:00 to 4:00.

(emphasis in original)

The first sentence shows physical movement; the others use the same terms but have nothing moving. Beyond movement, spatial metaphors permeate our thoughts and language. "The meeting is at 3:00", for instance. In fact, it's hard to describe almost anything without physical metaphors.

Many examples exist in software. Imperative programmers deal with pointers, objects, stacks, messages, events and so on. Our secondary storage uses folders, files and paths.

In fact, there is an alarming predictability on language adoption: languages that strengthen the physical metaphor -- or at least solve problems without weakening it -- have found widespread use. Languages with weaker physical metaphors remain with a small base. Compare the adoption rates of object-oriented languages to their functional counterparts.

This might be an indictment of functional languages, since they are typical expressed in abstract mathematics rather than physical analogs. That is unfair. Functional languages can be built and explained in straightforward physical terms -- they just aren't. Take, for instance, Haskell's Monad. I've written only small programs in Haskell, but for my purposes it's much easier to think of a Monad as a "to-do list". (Some use the term "action". I know it's an over-simplification, but that's the point.) The bulk of the language is a means to construct to-do lists in isolation. Put in these terms, Haskell can be simpler than many languages -- the functional purity eliminates all kinds of moving parts imperative programmers have to worry about.

This is a good step, but widespread adoption means more than renaming of concepts. Unfortunately, programmers who find and learn functional languages are not representative of the population. I'm afraid we would lose a broader audience in the first hour of learning Haskell, as soon as they got to this:

fibs = 1: 1: zipWith (+) fibs (tail fibs)

Beautiful, isn't it? One line of code for Fibonacci numbers, what amounts to built-in memoization, the ability to get and manipulate any sequence, and so on. But it also has only the weakest of ties to a physical analog. Non-trivial examples are much harder to understand. Sadly, an incredibly powerful tool for some is simply too abstract for others to use effectively.

Because of this and other examples, I doubt Haskell will ever be widely used despite all of its advantages. But this is okay -- we need powerful tools for the hands of experts. What I want to see is a more accessible language borrowing the best ideas from Haskell and others. I wish I could use Monads every day, and am willing to sacrifice some of Haskell's other power to achieve this.

This raises an important question: shouldn't we raise our expectations of developers rather than take away powerful features? A valid point, which I will tackle this in my next post. For now, the central thesis of these posts should start to emerge: we need a language with the best parts of Haskell and others in a more accessible form.