Saturday, June 03, 2006

Language-Driven Applications and the Command Pattern

When you go to write your next great application, consider imitating the work of CPU architects, where you design an Instruction Set Architecture (ISA) and programmer's register model. It would look something like this. Your application state needs to be clearly modeled, akin to the register and memory model of a CPU. Then the application operations would be distilled to a collection of commands, like machine instruction opcodes, which transform the data state. Then you provide a text interface to that command interface, like an assembly language. With this investment, consider where you'd be. First, by focusing on the application core and providing a clearly-defined way to manipulate its state, you get encapsulation from your user interface. Then, especially with the language interface, you allow extension of your application programmatically, third parties can script extensions in ways you never could have anticipated. And finally you can more easily employ the command pattern in your user interface or supporting environment to achieve features like undo/redo and macro record/playback. Do the hard work to make your application language-based, employ the command pattern, and I think you're digging a very solid foundation for a powerful application.

This language-based application is adopted by two notable applications, Emacs and Maya. Emacs, the text editor that doubles as a programming environment and mail reader, is based on Emacs Lisp, and the user interface is built around that language. Any user interface action, up to and including moving the cursor one character left or right, can be repeated by invoking the right procedure in the command window. Maya is the powerful 3D animation package that made the Lord of the Rings movie magic happen. Maya has a stunning user interface, beautiful in the way it makes users productive in an extremely complex environment. However, that interface is on top of MEL, the Maya Embedded Language, which the Lord of the Rings production team used to script "an enormous scene management system".

One benefit of the language based application is of course encapsulation. I suspect a large number of applications have major problems porting to new platforms because the application core and the user interface are too intimately tied. Mathematica, which actually is a programming language application, also happens to have an interesting and useful user interface, which handles things like typesetting and graph plotting. It goes to the extreme in encapsulation, where the user interface and command processor are actually separate processes. The command processor, or kernel, can run standalone and provides a simple text interface. There is in fact an entire transaction language (part of MathLink), for the read-eval-print loop interaction between the kernel and "the front end". It's an open standard, allowing potentially any number of completely different front ends to be built both proprietary and by third parties.

Another benefit of the language based application is future extension. Although there is the possibility of direct support for writing new commands in the language itself, exposing your application's core engine as accessible with a text-based script of commands allows people to write programs in their language of choice that generate "source code" for your application language. If you use Emacs you're probably familiar with the various modes that help you edit a particular file format or programming language, and many of these modes were not written by the same folks who wrote the Emacs core.

The command pattern opens up another handful of interesting features for your application. If your application core already has a suite of commands, they can easily be turned into function or command objects if they aren't already, each of which transforms the application from one state to another.

Probably one of the most useful features available with the command pattern is undo/redo. You keep a "command history" of the state transformations done in your application core. Undo takes the most recent entry from the command history, invokes the undo transformation, and places the entry onto the redo stack. Redo takes the most recent entry from the redo stack and invokes the original command that made the state transformation. The undo functionality may not come for free, but you may find that it's not that difficult and actually rounds out your command language. If you have an "add widget" command, you may not have noticed that your command language lacks the "remove widget" command until you need to support undo for "add widget." In other words, undo/redo commands may simply be pairs of complementary commands in your application language.

By persisting the command history entries to a file with atomic writes, you get a database-style log file. This permits you to recover from a process or machine crash, where you can start with whatever initial state is stored on disk, and replay the log to get back to the most recent log entry that made it to disk.

If you've already got a command history, you can give your application user "macros", in the style of Emacs, where the user designates points to start and stop recording user interface actions, and the resulting sequence of underlying commands can be replayed as an aggregate state transformation. It is in effect a user-defined program in your application's language. Many applications support some form of this feature, including the ability to edit the generated code after the fact.

By combining some of these notions, you get another wrinkle: incremental version control. If you're already keeping a persistent command history log, the user can specify a desired "save point" as the next version of the application state, complete with comments. Now you can provide a feature for a user to roll back to a previous application state, or even branch from a previously saved version into a new direction. This may suit your application well, if the sum of the log entries between two different versions is smaller than saving an extra complete copy of the application state, and undoing or replying those changes is not unduly time-intensive.

So, those are some thoughts I had about structuring applications. Make a clean internal design separated from the user interface and provide a text-based language interface to it.

No comments: