With - Bastard Son Of Loop

WITH is a macro that provides a general, conversational binding construct much like loop does for iterative constructs. It allows the programmer to mix and match simple variable bindings, special variable bindings, destructuring-bind, multiple-value-bind, flet, labels, with-open-file without constantly opening up a new indendation level for each new type of binding. It also allows a more convenient syntax for type declarations that places the declaration nearer to the variable being bound. Unlike loop it doesn't try to be exhaustive, but it does try to cover the basics, and it does allow a convenient hook for extending the syntax.

Note: WITH has recently been revised, and the new syntax is incompatible with the old syntax (albeit only slightly). The new syntax uses AND as the parallel-binding conjunction, and simple concatenation as the sequential-binding conjunction. A side-affect of this change is that the VAR keyword for introducing a new variable is now required. This change has the effect of making WITH look suspiciously like ML's LET (it probably didn't help matters that with now also allows :: as an alternative to AS, and IN as an alternative to DO).

If you just came here for the code, you can find it here.

As an example of with, the function below is part of the regular expression parser in the clawk system.

(defun parse-str (str)
  (let ((scanner (new-re-scanner str)))
    (multiple-value-bind (token value)
        (next scanner)
      (let ((seq (parse-seq token value scanner)))
        (multiple-value-bind (token value)
            (next scanner)
          (cond ((null token) (list 'reg 0 seq))
                (t (throw 'regex-parse-error
                          (list "Regex parse error at ~A ~A" token value)))))))))

Written using with, this becomes much more readable:

(defun parse-str (str)
  (with
     var scanner = (new-re-scanner str)
     vars (token value) = (next scanner)
     var seq = (parse-seq token value scanner)
     vars (token value) = (next scanner)
   do
     (cond ((null token) (list 'reg 0 seq))
            (t (throw 'regex-parse-error
                      (list "Regex parse error at ~A ~A" token value))))))

With accepts the grammar:

    with               <- WITH bindings [ DO | IN ] . body
    body               <- form*
    bindings           <- binding [ conjunction binding ]*
    conjunction        <- AND |
    binding            <- undeclared-binding [ declaration ]
    declaration        <- [ AS | :: ] type |
                          DECLARE declare-clause*
    undeclared-binding <- modal-var-bind-form assgn form
    modal-var-bind-form<- mode var |
                          mode ( var+ )
    mode               <- VAR |
                          SPECIAL |
                          VARS |
                          DESTR |
                          FN |
                          REC |
                          OPEN-FILE |
                          OPEN-STREAM |
                          OUTPUT-TO-STRING |
                          INPUT-FROM-STRING |
                          SLOTS |
                          ACCESSORS |
                          other-defined-mode

    assgn                 usually [ = | <- | := ], but can be defined
                          for each binding-mode.

(unless it doesn't, in which case you'll need to read the source).

The interpretation of the conjunctions is that binding clauses joined by and occur in parallel, while simply listing binding clauses one after another causes sequential binding. At present, parallel binding is limited due to the underlying language, for example variables and specials can be bound in parallel, but variables and functions can't. Also, some modes (such as vars and destr) aren't capable of parallel binding at all, at least not outside their limited domain within a single binding clause. I hope to remove this limitation in the future, but it doesn't seem to be a problem in practice, at least no more of a problem that it is without with.

The mode is a keyword (with-style keyword, not keyword-package keyword or lambda-list keyword) that selects the binding mode.

At the moment, I've got modes defined for:

  • var - Declares a lexical variable.
  • special - Declares a special variable.
  • vars - Expands to multiple-value-bind.
  • destr - Expands to destructuring-bind. assgn term be one of (=, <- := from in)
  • fn - Expands to flet.
  • rec - Expands to labels.
  • open-stream - Expands to with-open-stream.
  • open-file - Expands to with-open-file.
  • output-to-string - Expands to with-output-to-string.
  • input-from-string - Expands to with-input-from-string.
  • slots - Expands to with-slots. assgn term is one of (in of).
  • accessors - Expands to with-accessors. assgn term is one of (in of).

    The binding modes are definable via def-with-expander and def-with-alias macros. To assist in the definition of common cases, there are two auxiliary functions canonical-bind-expander and canonical-with-expander. See the bottom of with.lisp for plenty of examples.

    Here's the code, in gzipped tar format.


  • Michael Parker
    1