Friday, January 11, 2013

Fun, Work, Puzzles, and Programming

Some programming tasks are just more fun than others. The same thing extends to languages—why are Perl and Ruby so much more fun to work with than Python?

I suspect that the answer lies in the scope of the solution space, in a sweet spot between “too straightforward” and “too complex.”

Solution Spaces

The true point of programming is to solve external problems. Those problems typically have myriad solutions, because any Turing-complete language can decide any decidable problem, and the efficiency of it is just a detail. The work of programming involves pruning the solution space into one concrete solution (which is, naturally, the code artifact produced by the search. I’m going to assume a well-defined problem, even though those are rare in the field, because I want to focus on the solutions.)

The term “solution space” might be defined as “all possible programs in a given language, which solve the problem.” Further constraints like time budget and efficiency concerns offer ways to sort the set, in order to judge whether a given program is actually good or bad.

The solution space for a given language also depends on the features inherent in the language. Something like PHP 4 has a much narrower space than PHP 5, because the latter has a full object system. PHP 4’s OO support is more of a trivial hack that didn’t work very well in practice. PHP 5 offers you every choice PHP 4 did, in addition to familiar capabilities within the new object system. Lisp as a language offers an even wider space, because of CLOS, symbols, macros, and the condition system. Java has a fairly narrow space for its native language, but many other choices that can co-habit its VM, and one of the widest libraries of all.

Fun

A lot of fun can be derived from puzzles, where searching for a solution that fits the constraints at hand (self-imposed or otherwise) is engaging and rewarding. For simple enough problems or narrow enough solution spaces, it’s not hard to find an answer, and so it’s not so rewarding. OTOH, complex enough spaces can lead a programmer into a cycle of implementing and re-implementing different solutions to the same problem, just to find one that most suits them.

Perl golf is fun, because there’s so many tricks that can be brought to bear on it; it’s something that’s hard to imagine in a language like C#, and it clearly doesn’t fit the one-obvious-way-to-do-it Python culture. Ruby has a wide open feel, because it has an object system with a twist (open classes and eigenclasses), blocks, signal/catch, continuations, retry, symbols, everything is a value, and rich function parameters (including *args and option hashes). All those things add up to making the solution space much wider than other contemporary languages.

Golfing shows up again in efforts to create minimal languages, or operating systems, or filesystems, whatever: any metric for any thing that can be minimized, will be minimized. If management wants fewer LOC, they’ll get a smaller number of horribly tangled and magical LOC that ends up being counterproductive.

Puzzles and Spaces

Puzzle languages can be roughly defined as languages in which turning a solution into the native code type is a puzzle. On the other hand, a better definition might be that they’re languages in which the solution space is fairly disjoint from mainstream languages. Everything you know about Java is pretty much useless in Forth or Haskell or J.

Clojure allows for less puzzling where desired, although it does have its own philosophy and a Clojure solution can be phrased according to any of several paradigms. In essence, the multi-paradigm aspect lets Clojure’s solution space unfold along several axes of style at once, including stream programming (a natural fit for parallelism via CSP), functional programming, more traditional OOP style, and even falling back to underlying Java types and objects.

Still more mainstream languages are more strongly focused on mainstream paradigms like OOP. Looking at Java, the designers clearly prefer the class-based, single-inheritance object system over all other solutions. You can certainly build a small Forth in Java and code a solution in terms of that interpreter, but then you’re not working “in Java” anymore.

Although puzzle languages aren’t popular in the mainstream, sometimes elements of them crop up in ordinary work with mainstream languages. It’s just that those problems are small, controllable parts of the system. They seem manageable up close. While I wouldn’t even know where to start looking to figure out how to write a FastCGI application in Forth, every single mainstream language knows how to push data through TCP sockets, and most of them have some sort of library for talking FastCGI over one. On the other hand, I’m pretty sure monkey-patching MIME::Lite to force SMTPS was the hard way around compared to replacing MIME::Lite with Email::MIME and Email::Sender.

It would look as if the SES API backend that sends the main website’s email were the long way around, but Amazon didn’t introduce SMTP integration until the AWS-related segment of that project was nailed down. Much of the work ended up happening in making Email::MIME work in a way that suits me, and converting from print SENDMAIL "--$mime_boundary\n"; style. All of that would have happened either way.

Mountains from Molehills

Some programmers have a tendency to take boring problems, turn them general enough to be complex, and then solve those. (I can’t say I haven’t caught myself doing it.) The attraction of the puzzle and the rush of solving it seem to be the forces underlying that tendency. There’s also a desire to do speculative work to reduce the need to do work later. Playing with a solution to a self-made problem like that is a lot more interesting than waiting until the future arrives to try solving the problem, because the illusory version isn’t faced with the hidden unknowns that make the future so much more difficult.

Of course, the amount of uncertainty in the future is also what makes the speculative work so inefficient. Rarely does a preconceived solution perfectly suit the problem it was designed for. This tends to be especially true of things like swappable database backends—if it hasn’t been tested on any other database but the first, then the design isn’t proven, and all you’re doing is preparing for a major debugging session on code that will be unfamiliar by then.

All work and no play

So there you have it. Programming without fun is dreadfully dreary; I’ve gotten angry with the world off-hours during times when work consisted of dealing with stacks of deficiencies of code I’m trying to use. When a long series of solutions are thwarted, then the difficult part is being done over and over again, but without reward. (In fact, when a perfectly good one is thwarted because of known bugs with patches filed and ignored for months or years, it’s an anti-reward: knowing I have solved it except that I have to take on a source build and patch for time indefinite.)

On the other hand, too many self-imposed conditions can turn a problem into something too difficult or even impossible to solve. It takes a lot of experience to tell the difference between “has become too difficult to solve reasonably soon” and the regular coding process of “need to keep solving bugs that show up as we go.” They tend to look similar in the trenches.

It seems, in the end, that staying engaged with my work means keeping it at least somewhat fun, carefully balanced between “mindlessly simple” and “dangerously hard.” But it’s also a hard place to stay, because estimating is hard; problems can be unexpectedly tricky.

No comments: