On Minimalism

Introduction

Today, I want to talk about something I've been meaning to get around to for a while. Specifically, I want to mention some realizations I've had about restrictions-as-strengths as it relates to programming languages. This blog post was so long in coming in part because I had conflated that issue with a desire to also discuss the source of brittleness (or non-modularity) in programs and how this all tied back to the foundations of programming and CS. Obviously, that's too much for one post. There are three questions here and each is important to me. Later, I'll blog about the other issues and hopefully write a summary to tie it all together.

Background

Programming is really hard. We've known this for a while. As Hal Abelson said, "Anything with Science in the name, isn't." Software engineering is no better off. Certainly no one would argue that we know how to build software as well as structural engineers know how to build bridges. Difficulty in software doesn't stem from a single source but as programmers we need to localize it as much as possible. One way we do this historically has been through tools: Our programming languages with their compilers, profilers and debuggers, our operating systems, and other bodies of code to reuse (libraries).

The Meat

Software has many demands placed upon it. First and foremost it needs to be functional by which I mean correct and relatively stable. Beyond that it needs to be reasonably fast and there are often other concerns from user friendliness to security. All of these concerns introduce complexity to our software, that complexity needs to be managed and I think a central question in managing that complexity is how to partition and quarantine it. I did a rather poor and embarrassing job of at least raising that question a while back. I was lucky enough to find an outstanding attempt at an answer on Daniel Lyons' blog. It was completely coincidental though, he hadn't read my article.

Daniel framed this problem as one of seeking minimalism. I see the same answer from a slightly different angle. To me, there seems to be a pattern of trying to handle complexity by restricting the actions of the programmer. For example, in Peter Seibel's Coders at Work there are numerous mentions of how different subsets of C++ are chosen by different teams of programmers to reduce the complexity of overlapping or interrelated features. People will entirely abandon templates or operator overloading. Douglas Crockford mentions making sure to never use the continue statement in his code. These are examples of programmers simplifying their own mental model to make problem solving more tractable.

Languages do this too of course, the most prominent examples from my very limited experience being Haskell forcing you to think in terms of its type system or Factor forcing you to think in terms of its stack. Adapting to the constraints may be awkward or difficult at first but they do provide real benefits. The Glasgow Haskell Compiler is capable of remarkable optimizations because it can assume that you've adhered to the restrictions of immutability and laziness. Judicious use of the type system can eliminate entire classes of possible bugs and restricting the use of mutable state simplifies parallel programming and concurrency. Through use of the stack, Factor strongly encourages thinking about the dataflow of your program. I've heard this sort of thing expressed before as a language being opinionated about how to solve a problem and there are plenty of diatribes on the failure of one paradigm (opinion) or another, especiallyOO since it's been dominant for so long. But let's not confuse this issue as being about particular paradigms or programming languages.

There are languages on the other end of the scale. I tend to think of Common Lisp as the primary one. (Disclaimer: I've written far more code in CL than anything else. My coding experience in every other language is positively trivial by comparison.) Common Lisp has been described by many others as being agnostic or happy to let you express a problem any way you can think how. Then again, Common Lisp requires you to think in Abstract Syntax Trees. It's the rift between opinionated and unopinionated languages that I'm curious about. Of course, Haskell and Lisp are (generally speaking) solving different problems as lispm (Rainer Joswig) notes on hackernews.

Vladimir Sedach suggests that the rift is about metaprogramming. More specifically, he states that Lisp and Smalltalk are self-describing computational systems. Lisp is written in Lisp. Smalltalk is written in Smalltalk. It's that old metacircularity/homoiconicity chestnut. Furthermore, he mentions that Haskell can't be self-describing because it's built on two computational systems. The type system is one and the language built atop it is another. Besides as Vladimir says, "If type systems could be expressed in a metacircular way by the systems they were trying to type, they wouldn't be able to express anything about types (restrictions on computational power) of that system." Factor's FAQ even mentions that they avoided purity and static types to enable the benefits of metaprogramming and interactive development. Personally, I know what I miss most in Haskell is an environment like SLIME. Interactive development of that style has an entirely different feel to me.

These observations about types and metaprogramming were a revelation to me and clearly, it's a limitation on the expressive power of something like Haskell. The question seems to remain open however as to whether or not such restrictions are enough of a strength to offset their cost. It seems to me that it must be situationalbut I'm interested in a more in-depth examination of the problem. Unfortunately, I can't offer such an examination myself today. Googling around a bit I found a discussion has started on hackernews about lisp while I wrote this. In the discussion jerf writes largely about this issue. Jerf suggests that the other end of the restricted/unrestricted scale is Java which sounds about right to me. He also suggests that the problem with maximum empowerment languages is that they don't scale to large team sizes and (correspondingly) large programs.

Of course, there are counterexamples like ITA Software but macros were thoroughly debated even among friends at ILC 09 as harmful or helpful to team programming. Vladimir Sedach again has a good grasp on their utility. In my opinion, neither Factor nor Lisp has resolved this question yet but more companies are getting involved with metaprogramming whether it be in Ruby or something else. I hope methods of containing the destabilizing facets of metaprogramming will emerge. Where Common Lisp is concerned, I think Fare's XCVB is an interesting opportunity in this direction and I'm watching it intently.

The Takeaway

Enabling single programmer productivity is important but so is enabling teams. Java as an extreme example of restriction has caused at least as many problems as it has solved. Languages with powerful metaprogramming features often need to be restrained in some fashion when used by large teams. There must be a middle ground somewhere. I say this because eventually our systems (codebases) become huge and unwieldy and we need our languages to support the difficult task of controlling that complexity and of keeping them modular and malleable. That problem of verbosity and inflexibility is precisely what metaprogramming tries to solve. I'll write about the problems in achieving modularity more in my next post.