May 3rd, 2010

Named arguments

Another new feature in the next update is something I'm calling named arguments.  It might not be obvious at first glance, but this is designed to be a convenience feature, to address a particular inconvenience that's always afflicted TADS programming.

Let's start with the mechanism - I'll get to the motivation in a bit.  The basic idea is that when you call a function or method, you can include one or more "named" arguments.  These are given explicit names in the call, with the syntax "name: value".

  doSomething(a: 1, b: 2);

The callee uses similar syntax to specify receiving the named values; they just leave off the value part.

  doSomething(a:, b:)
    "This is doSomething: a=<<a>>, b=<<b>>\n";

Since the arguments are named, you can put them in whatever order you want and get the same result: doSomething(b:2, a:1) is the same as doSomething(a:1, b;2).  You can freely mix named and positional arguments: doSomething(a: 1, b:2, 3, 4).

This sort of thing exists in several mainstream programming languages.  The main motivation in other languages is that it makes code more self-documenting by spelling out which arguments are tied to which parameter names, which is especially useful in functions that takes gobs of arguments, such as you typical Win32 API.  But the TADS feature has two other important details that makes it very different from the usual named argument mechanism.

First, a callee doesn't have to declare all of the named arguments a caller sent.  You can define doSomething as just doSomething(), and it'll work fine - no error from the compiler or run-time.  Second, named arguments "pass through" callees to their grand-callees, and their great-grand-callees, etc.  In other words, if doSomething() calls doSomethingElse(), and doSomethingElse() calls doAnotherThing(), you can do this:

  doAnotherThing(a:, b:) { "Hey! I inherited a=<<a>> and b=<<b>>!"; }

Python [er, make that] Perl is the only language I know off-hand with anything similar.  It has dynamic local variable scoping such that a callee inherits the local variables of its caller, grand-caller, etc.  Python Perl essentially treats the call stack the way C-like languages treat lexical scope.  TADS named arguments are basically like that, but only named arguments - ordinary local variable scoping continues to be C-like.

This probably seems a little bizarre and out of the blue, but it's the first decent solution I've been able to come up with to a problem that's vexed me since the TADS 2 days.  (At least, I think it's a decent solution.  We'll see if anyone else agrees.)  The problem is peculiar to systems like TADS where you primarily write a program by extending an existing class framework.  If class frameworks for GUI programming had caught like they were supposed to, C++ might have tackled this problem by now; or maybe if C++ had tackled the problem class frameworks would have caught on.  But I digress.

As I said, you program in TADS largely by subclassing library classes and overriding methods.  If you want to describe a room, you create a Room instance/subclass, and you override the desc method.  When it's time to describe the room, the library calls your override.  It's an inversion of the traditional relationship between software libraries and their users: traditionally, the user code calls the library.  Here, the library calls user code probably about as much as vice versa.

So here's the basic problem I was trying to solve: the library often has a whole bunch of context information when calling one of these overridable methods; how much do you pass to the callee as arguments?  For example, when generating a room description, the library knows who's looking, how far away they are, what the light levels are like, what's in scope, etc.  Some or all of this information is sometimes useful in writing a room description; but mostly it's not.  Much of the time you just want to write out a simple static message.  It would be a huge pain if you had to write something like

  desc(pov, distance, brightness, scopelist, verb) { "It's a fairly boring empty room."; }

every time.  I mentioned that this problem goes back to tads 2: those of us who used tads 2 will recall that every verb handler had to have an actor parameter, and sometimes an other-object parameter:

  verDoPutIn(actor, io) = ...

That was tedious, which is why it was a top priority in Adv3 to find some other way to pass that command context information.  In Adv3, the solution was global variables - gActor, gIobj, etc.  And in fact that's the pattern that Adv3 uses with a fair degree of consistency for this kind of situation.  Some other examples are callFromPOV() and callWithSenseContext().  There's a lot of this kind of thing in the library:

  local oldFoo = gFoo;
    gFoo = newFoo;
    gFoo = oldFoo;

It's not pretty, but it works.  Apart from the inelegance, though, it has a couple of serious downsides.  One is that it's such a pain to write all that code that there almost has to be a dedicated library routine each time the pattern is used, which bloats the library and is somewhat limiting to user code.  Another problem is that it's ad hoc and unstructured - there's no formal way of knowing if a global variable is valid at any particular time, so you might accidentally use an old value.  Third, this ad hoc approach doesn't protect against other code clobbering your globals unexpectedly - using the save-try-restore pattern ensures that you play nice and don't clobber other code's globals, but it doesn't protect you.

The named argument scheme is basically a structure replacement for that try-save-restore pattern.  It's plainly a lot more concise.  To me, at least, it fixes the inelegance problem; the syntax is straightforward and a fairly natural extension of the existing call syntax, and I think it's much easier to see at a glance what's going on than with the try-save-restore business.  Since the mechanism is uniform, you don't have to worry about other code playing nice - other code basically has no choice but to play nice, and can't unwittingly clobber your variables.  It also solves the problem of knowing if a variable is valid at any given time: the system will throw an error if a callee declares a named argument that doesn't exist anywhere in the call stack.  That lets you tell immediately that you're doing something wrong, without having to accidentally discover it via a nil deref or (worse) a stale value.

I don't intend to retrofit this into Adv3, even though there are a lot of places it would be a great improvement.  However, I am using this throughout the "Mercury" library I've mentioned here.  That'll be the real proving ground for it, and I expect that it'll be a big part of achieving the Mercury goals of easier to learn and use.