From oleg-at-pobox.com Tue Oct 27 12:54:42 1998 From: oleg-at-pobox.com X-Amended: Feb 4, 2005. Added references to advanced implementations of keyword arguments in Scheme and Haskell Subject: Implementing keyword and default arguments, function's resource database Date: Tue, 27 Oct 1998 20:55:50 GMT Keywords: keyword argument, positional argument, Scheme, C++, Perl, function call, binding environment Newsgroups: comp.lang.dylan,comp.lang.clos,comp.lang.scheme,comp.functional Organization: Deja News - The Leader in Internet Discussion References: <3625FD45.8BDF4023@ubs.com> <362E0106.4D5BEE1E@altera.gr> <362F11DA.CF5F58E6@altera.gr> <87vhlcr9tp.fsf@dent.isdn.cs.tu-berlin.de> <363066F5.BEB6C77@altera.gr> <36349F0D.41C6@bogus.acm.org> Summary: Examples of implementing keyword arguments in various languages X-Article-Creation-Date: Tue Oct 27 20:55:50 1998 GMT Status: RO When a function is called with positional parameters, its argument list is, well, a list. It's a list indeed in Scheme/Perl, and to some extent in C/C++ keeping in mind a stdarg/varargs facility. To permit keyword parameters, the argument list should become an associative list, or a "dictionary" where arguments are to be looked up. This insight helps implement keyword and default arguments in any language. Scheme, C++, Perl and a bit of Haskell are considered in this article. For example, in Scheme: ; two keyword arguments: 'from and 'to (define (subtract . L) (let ((from (cond ((assq 'from L) => cdr) (else 0) ; default value for 'from' )) (to (cond ((assq 'to L) => cdr) (else 0) ; default value for 'to' ))) (- from to))) (subtract '(from . 10) '(to . 1)) ==> 9 (subtract '(from . 10)) ==> 10 (subtract '(to . 1)) ==> -1 (subtract) ==> 0 (let ((x 10) (y 1)) (subtract `(to . ,y) `(from . ,x))) ==> 9 I confess the syntax is unwieldy, yet the above snippet simulates all the properties of keyword arguments. The technique is easy to generalize to handle both positional and keyword parameters. A macro (syntax special form) may make these "keyword" arguments more presentable. BTW, in Gambit-C, keyword arguments are available right out of the box. Thus one can write (like in DSSSL) (define (sub #!key (from 0) (to 0)) (- from to)) (sub) ==> 0 (sub from: 10) ==> 10 (sub to: 1) ==> -1 (let ((x 10) (y 1)) (sub to: y from: x)) ==> 9 Gambit's documentation gives a more thorough example. [Added Feb 2005] Please see ./macros.html#keyword-args for a portable, R5RS-compliant implementation of keyword arguments for macros (and first-order procedures). Keyword resolution is done at macro-expand time. When implementing keyword and default arguments in C++, one has a choice: the argument "dictionary" may be constructed and searched either at run time, or at compile time. The first solution is rather trivial and straightforward: it amounts to building an associative list (or a hashed array) of char * key; int type_tag; union { int int_value; double double_val; char * string_value;} value; nodes. A function then searches this "dictionary" for the bindings it needs. Construction of this "argument environment" and the lookups can be done entirely at compile time: #include template int sub(const Args& args) { return args.get_from() - args.get_to(); } class Args_Positional { int from; int to; public: Args_Positional(const int _from, const int _to) : from(_from), to(_to) {} int get_from(void) const { return from; } int get_to(void) const { return to; } }; class Args_Keyed { int from; int to; public: Args_Keyed(void) : from(0), to(0) {} // Default values int get_from(void) const { return from; } int get_to(void) const { return to; } Args_Keyed& set_from(const int _from) { from = _from; return *this; } Args_Keyed& set_to(const int _to) { to = _to; return *this; } }; main() { cout << "Positional args: " << sub(Args_Positional(10,1)) << endl; cout << "Keyed args: " << sub(Args_Keyed().set_from(10)) << endl; cout << "Keyed args: " << sub(Args_Keyed().set_to(1).set_from(10)) << endl; } The code prints as expected: Positional args: 9 Keyed args: 10 Keyed args: 9 A run-time overhead is almost non-existent as a smart compiler can inline all the code. Thus the parameter lookup is done at compile time indeed. Note that if an Args-type parameter the function sub() receives does not contain methods get_from() and get_to(), we get a _type_ error at compile time. This technique works just as well in Haskell. In fact, it has been extensively used in Fudgets. Let me quote from Magnus Carlsson and Thomas Hallgren's awesome doctoral thesis: myFudget = displayF setFont "fixed" >+< buttonF (setFont "fixed" . setKeyEquiv([Meta],"q")) "Quit Haskell's type checker makes sure that a customizer (setFont, SetBGColor, etc) is appropriate for a particular Fudget. This is a remarkable feature. [Added Feb 2005] Please see ../Haskell/types.html#keyword-args for a genuine, strongly-typed, very DSSSL-like implementation of keyword arguments in Haskell. The implementation uses type-level reflection. In Perl, arguments passed to a subroutine are all packed in a single list (which the subroutine can access as @_). Making this list associative -- a hash -- instantly permits keyword parameters. That's what a "new style" of calling Perl functions is all about: ~> perl -e 'sub subtr { my %a = @_; my $from = $a{from} or 0; my $to = $a{to} or 0; $from - $to} print subtr (to=> 1, from=>10)' prints 9. I could write the code above more succinctly (if more arcane) as ~> perl -e 'sub subtr { my %a = @_; ($a{from} or 0) - ($a{to} or 0)} print subtr (to=> 1)' It was mentioned above that a set of arguments a function receives forms a dictionary, a binding environment. The following equivalence is a clear illustration of this point: ((lambda (x y) (- x y)) 10 1) <==> (let ((x 10) (y 1)) (- x y)) This prompts yet another implementation of keyword arguments: (define-macro (sub2 . bindings) `(let ((from 0) (to 0)) ; default values (let ,bindings (- from to)))) (sub2) ==> 0 (sub2 (from 10)) ==> 10 (let ((x 10) (y 1)) (sub2 (to y) (from x))) ==> 9 This construction may only be of a passing theoretical interest. Yet it is probably more efficient that the Scheme implementation above. The examples in this article hopefully show that an argument "list" indeed looks and acts as an environment a function looks up the parameters it needs. This environment is flat, in the above examples. One can however make it more structured. A function can take a sequence of "argument environments", which correspond to defaults and profiles of different levels of "generality" or scope. If a lookup of a parameter fails in one dictionary, the "next" environment is tried. This looks amazingly similar to a 'delegation-based' object-oriented system. I can rant about this on and on; in fact I did in a "paper" http://pobox.com/~oleg/ftp/papers/Database_as-a_language.ps.gz ""Database as a (OO) language" which ended up nowhere. The system of multi-level defaults and profiles mentioned above is being actually used in a production environment: http://pobox.com/~oleg/ftp/Scheme/Request-Language.html