Today's project was something of an object lesson in programming languages. It was fun, almost an adrenaline rush of hardcore programming. And it wouldn't have been possible in many languages.
I've known this was coming for a long time, but yesterday was the final straw. I was adding a new function to QL (Querki's programming language), named _createHere, which creates a new Thing whenever it is displayed. I needed it so that I could add a button on the page that creates a new Issue entry in my comic book Space and inserts the editor for that new Thing right after the button:
[[_QLButton(Issue -> _createHere -> _edit)]]
Thing is, though, _createHere was fundamentally broken -- or worse, it worked, but in a way that put the whole system in danger. The process of creating a Thing has to go to the database: it's a slow, blocking operation, and the first rule of writing scalable code is Thou Shalt Not Block. Querki isn't quite as faithful to that as I'd like, but I'm painfully aware that every exception is, by definition, a bug. Displaying the above expression would block for (by my standards) a horribly long time, tying up a thread, consuming system resources, and generally being ugly.
So yesterday afternoon I set out to do something slightly insane: rewrite the entire QL pipeline to be asynchronous. The only way that _createHere could not break the system would be if every single Stage in QL -- the stuff between the arrows -- was non-blocking, if any one of them could return a Future to be completed later. This was dangerously audacious even for me: this code is called in *hundreds* of places in the code, everywhere that anything gets displayed. Making this work would require changing all of those calls, correctly.
I timeboxed one full day to pull it off, as a grand crazy experiment. I did pull it off, rewriting dozens of files -- probably hundreds of different functions -- in eight concentrated hours of code. And so far, it seems to have introduced only one bug, which was quickly fixed. That isn't because I have Mad Programming Skillz, it's because of having the right tools.
Part of that is intrinsic to a strongly-typed language. I make a change over here, and the compiler tells me that I've broken the code over there. Heck, I don't need to go to the compiler -- the IDE tells me about most of the problems before I even try to compile it.
And the other half is having the right abstractions in that language. Traditionally, asynchronous programming is an unbelievable pain in the butt. I call asynchronous function F, pass in callback C, which gets invoked when the F is done, and things chain together like that. Understanding code like that is nightmarish, and refactoring it is worse.
In Scala (and other modern languages), that idea is reified into a data structure. When you make an asynchronous call that returns a T, you get a Future[T] -- a handle from which you can get a T once it's ready. You can combine these: right after making the first call, you can combine it with a function that takes a T and returns a U, and then you've got a Future[U].
Etc, etc -- almost any way you might want to compose Futures together, you can. If I have a List[G], and a function that takes a G and returns a Future[T], I can get a List[Future[T]] -- I'll get a bunch of Ts later. And then I can use Future.sequence() to turn that inside-out, into a Future[List[T]] -- once they're all ready, I get the transformed list that I actually want.
The result is that I can take hundreds of functions that return various types, and change one of them to be asynchronous. The compiler tells me that since this is now returning a Future[T] instead of T, all the calls have to change. Some of them need to be restructured a little, but it's usually pretty mechanical. Each of those changes causes the compiler to tell me to make other changes, until I hit the actual edges of the threads -- either back at an Actor or returning a value to the Client, both of which are inherently thread boundaries.
Keep pulling at the threads until the types all line up again, run -- and it all still works. Hundreds of changes in a focused eight-hour slam of coding, and it all still works. Nothing has changed from the user's point of view. Only now the system is properly scalable, where it wasn't before, and I have a world of asynchronous functions like _createHere that I can now safely add.
The one bug? That was the exception that proves the rule. One function was fetching a name and then immediately printing it. *Everything* on the JVM is printable, so the compiler is perfectly happy to do that -- it doesn't know that it's doing something wrong, and simply prints out "Future" instead of the name. The moral of the story is that the one bug was caused by one of the few places in the code that isn't strongly typed.
So, yeah. In a more weakly-typed language, this project would have taken a man-month. In Scala, it took a day -- a hard day, but a good one. And that is why a big, serious project that is intended to be developed and maintained for a long time should always be written in a solid, strongly-typed language...