Tuesday, May 24, 2005

Argument order

For multiple-arity functions, it would be more abstract to pass arguments by label rather than order. Just like pattern matching is more abstract than using explicit selectors, passing in arguments by label allows you to pass arguments in any order, and would allow for a more general form of currying. It would also simplify partial evaluation, since you wouldn't have to rearrange a function's arguments to apply it to a subset of them.

Nevertheless, tuples are a lighter-weight data structure than records, and lighter-weight solutions are easier to write and often easier to read. Positional function arguments are here to stay.

Because the order of arguments isn't always particularly relevant, the order can seem arbitrary. As the designer of a procedure, it's sometimes hard to know what order to choose. Here's one criterion: think about currying. It can be useful to put the arguments expected to be known earlier before the arguments expected to be known later.

For example, in Syntactic Abstraction in Scheme, the heart of the definition of the macro expansion algorithm is the rule for macro applications:
expand(e, r) =
  case parse(e, r) of
    ...
    macro-application(i, e) → expand(mark(t(mark(e, m)), m), r)
      where t = r(resolve(i)) and m is fresh
This is pretty hard to read. Without getting too much into the specific details of the algorithm, the idea of this rule is that to expand a macro application, you mark the expression with a fresh mark, apply the macro transformer t to the marked expression, then mark the output of that transformer again with the same mark, and finally expand the result.

There's a functional pipeline buried in that rule, but it's obscured by the order of arguments. This is actually a good candidate for point-free style. Since we expect the environment argument r for expand to be known before the expression, and the mark argument m for mark to be known before the expression argument, let's swap the order of arguments to both those functions. Now we get the new rule
...
macro-application(i, e) → ((expand r) ⋅ (mark m) ⋅ t ⋅ (mark m)) e
...

1 comment:

Anonymous said...

I disagree with your first paragraph. Sure, passing arguments by label is independent of the order of the arguments in the function definition.

But one could just as easily say that passing arguments by position is independent of the names given to those arguments in the function definition. (Allowing distinct labels and identifiers wouldn't really change my argument.)

And is call-by-label really necessary for a more flexible form of currying? Haskell addresses this for binary operators with their cut syntax; I suspect one could extend this idea to cover higher-arity functions as well. If we have a function f :: W -> X -> Y -> Z and values w :: W and y :: Y, then we could say something like f w _ y, which would have type X -> Z. (Granted, underscore is probably a bad choice for this given its use as the non-binding pattern, but you get the point.)

Since the theme of this blog appears to be finding the general underlying principle, let me propose this: any time you have two elements of a program communicating, you have to break abstraction between those two elements at least enough to define a communication mechanism. This applies to passing arguments to a function, importing and exporting things across module boundaries, communicating between multiple threads in one process, or communicating between multiple processes over a network.