[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

highly parametric interfaces



Felix raised a good point, which is that many Schemers, including
those that regularly use keywords, feel at some level that they're a
bit of a hack.  Keywords are purely syntactic sugar, and add
complexity to the language core.

Yet we use them.

Even if not in the literal sense of a self-evaluating literal with a
colon stuck on one end, sooner or later most people come across a
procedure with too many parameters to simply be tacked on as
optional arguments.  Currently there are many ways to handle this:

* Functional purist - "Your design is wrong, there's no excuse ever
  to write a function with such varied behavior."  This person may
  in fact be correct.  Using combinators and monads, any
  parametrized function can be broken into smaller, more manageable
  components.  However, finding the appropriate breakdown can be
  time-consuming and difficult.  Moreover, some interfaces genuinely
  are more naturally expressed with parameters.  Anyone who would
  argue this is probably already using Haskell, or at any rate
  should just ignore this SRFI and never use it :)

* OOP purist - "Just wrap any parametric behaviour into one or more
  configuration objects."  This is very flexible, and convenient for
  chaining and passing on the same (or slightly modified)
  configuration to other functions.  Early discussions on the RRRS
  authors list included an interesting variation of the
  NUMBER->STRING procedure:

    (number->string <number> <format>)

  Formats included (FIX n), (SCI n m), and more complicated
  compositions such as

    (number->string n (polar (flo 2 (radix 8)) (flo (radix 10)))

  The performance minded individual would make the configuration
  object constructors integrable.  [The original discussion wasn't
  clear but it seemed to imply they were syntax, which destroys
  compositionality and loses much of the flexibility you've gained.]

  The disadvantages of this are that either 1) for every parametric
  procedure you need to define a new class and keep it in sync as
  the API changes, or 2) you use a single extensible configuration
  class, perhaps a hash-table or closure, which suffers from poor
  performance.  Perl effectively uses the latter, passing
  hash-tables as arguments to complex functions, but Perl's
  hash-table syntax makes this feel more like a keyword interface,
  discussed below.

* Efficiency-oriented OOP - "Just set the parameters in the calling
  object."  This person of course assumes there are no functions,
  only methods, and only for a single calling object.  This is where
  you get:

    (define fmt (make-number-formatter))
    (number-formatter-set-complex-behavior! fmt O_POLAR)
    (number-formatter-set-magnitude-style! fmt O_FLOAT)
    (number-formatter-set-magnitude-places! fmt 2)
    (number-formatter-set-magnitude-radix! fmt 8)
    (number-formatter-set-angle-style! fmt O_FLOAT)
    (number-formatter-set-angle-radix! fmt 10)
    (number-formatter-format fmt n)

  for the equivalent to the above NUMBER->STRING example.  These
  people are all coding C++ or Java, so we can ignore this and not
  even discuss what's wrong with it.

* Lisp minimalist - "Why does everyone keep introducing new types
  when a simple alist will suffice?"  Again, this is true, you can
  easily represent everything with an alist:

    (number->string n '((radix . 10)))

  This is fully general, and can express nested formats like the
  polar example naturally.  Unfortunately, for more typical examples
  this is usually going to involve backquotes and commas

    (button `((text . ,(gettext "OK")) (action . ,quit)))

* Syntactic sweet tooth - "Lists are fine, but let's clean up the
  appearance a little."  Rather than an alist, a simple plist is
  just as efficient and can look much nicer:

    (button 'text (gettext "OK") 'action quit)

  Moreover, if we use a common convention for the keyword symbols we
  can visually distinguish (and have our editors automatically
  highlight) keyword usage:

    (button 'text: (gettext "OK") 'action: quit)

------------------------------------------------------------

The last example is what the Lisp community overwhelmingly turns to
in these situations, but there is sufficient variation that is seems
worthwhile standardizing.  Some of the current practices are:

  :keyword  - prefix, self-evaluating symbol as in CL
  :keyword  - prefix, distinct type as in many Schemes
  keyword:  - suffix, distinct type as in many other Schemes
  #:keyword - non-conflicting prefix, distinct type
  keyword   - syntactic keyword as in SSAX

The last is really disturbing.  Here the author was faced with an
SSAX:MAKE-PARSER procedure accepting up to 7 optional callback
arguments, trying to write a portable library, and wanting to keep
the interface simple.  Because there is no portable keyword
interface, SSAX:MAKE-PARSER became a macro, automatically quoting
the keyword symbols.  This means SSAX:MAKE-PARSER isn't composable,
which has been a source of frustration to me.

For this reason alone there should be a keyword SRFI!

[I'm personally leaning towards

  'keyword:

i.e. just using symbols, with a : suffix as a convention.  No need
to add new types, and we can have a portable OPT-LAMBDA or
LET-KEYWORDS* form.  On the other hand, a separate type can help
catch errors.  Either way I'd probably just go with the masses
here.]

At the same time, Felix's concern is very valid.  We shouldn't use
keywords everywhere just because we can.  Many of CL's functions
take only one or two keyword arguments which could just as well be
passed as an optional argument.  And I think most Schemers would
prefer

  (assoc elt ls my-equal?)

to

  (assoc elt ls test: my-equal?)

At the other extreme, SSAX and GUI interfaces clearly demand keyword
API's.  In the middle are interfaces like hash-tables and ports,
which could be argued either way.  We'll just have to fight those
out :)

--
Alex