Variable-arity functions in Lisp

One of the handier features of modern functional languages like Erlang and OCaml is the use of unification to match arity against function definitions. The ability to define a function in terms of the type and number of arguments passed is both expressive and useful.

Common Lisp’s destructuring-bind uses unification to bind local variables, much like let. Given a list of arbitrary depth, destructuring-bind can “extract” its elements and bind them in a lexical block.

(setf lst '("foo" "bar" ("baz" "bat")))
(destructuring-bind (a b c) lst
    (format t "~A, ~A, and ~A" a b c))
; => foo, bar, and (baz bat)

newLISP has a unification function and a bind function that can do much the same thing.

(set 'lst '("foo" "bar" ("baz" "bat")))
(bind (unify '(A B C) lst))
(println A ", " B ", and " C)
; => foo, bar, and ("baz" "bat")

Unfortunately, attempting to use a unification in a local scope does not work so easily:

(let (unify '("foo" "bar" ("baz" "bat")))
    (println A ", " B ", and " C))
; => symbol is protected in function let : unify

This can be worked around with a macro and letex, which provides similar functionality to Common Lisp’s macro template syntax (except that it uses quoted, locally bound symbols, rather than backticks to insert values):

(define-macro (destructuring-bind)
  (letex ((unifier (args 0))
          (target (args 1))
          (body (rest (rest (args)))))
    (let unifier
      (bind (unify 'unifier 'target))
      (dolist (expr 'body)
        (eval expr)))))

This is a reasonable approximation of the CL destructuring-bind macro, although don’t try and pass (args) directly to it (this is a limitation of macros in newLISP; (args) would evaluate to a list of destructuring-macro‘s arguments, not the calling function’s.

This implementation locally initializes the variables to be unified to nil and then binds them to the result of unify. Once bound, it iterates over the expressions in the body of the form.

cond can be used to match against various patterns of arguments passed to a function:

(define (foo)
    (cond
        ((unify '(A B C) (args)) (do something))
        ((unify '(A B) (args)) (do something else))))

This makes routing of different arities possible, but not convenient. With a couple of macros, we can go a step further and create a destructuring-case conditional that accepts the target list for unification and a series of conditionals, as with cond:

(define-macro (cond-form)
  (letex ((target (eval (args 0)))
          (unifier (nth 0 (eval (args 1))))
          (body (nth 1 (eval (args 1)))))
         '((unify 'unifier 'target)
            (destructuring-bind unifier target
              body))))
 
(define-macro (destructuring-case)
  (letex ((target (first (args))) (conditions (rest (args))))
         (eval (cons cond (map (fn (c) (cond-form target c))
                               'conditions)))))
 
(set 'lst '("foo" "bar" ("baz" "bat")))
(destructuring-case lst
  ((A B C) (println A ", " B ", and " C))
  ((A B) (println A " and " B))
  ((A) (println A)))
 
; => foo, bar, and ("baz" "bat")

Although this does not permit a function to be defined multiple times in terms of its arity, it does enable equivalent functionality, albeit within the function body:

(define (test)
  (let ((params (args)))
    (destructuring-case params
      ((A B C) (println "Case 1: " A ", " B ", and " C))
      ((A B) (println "Case 2: " A " and " B))
      ((A) (println "Case 3: " A)))))
 
(test "foo" "bar" '("baz" "bat"))
; => foo, bar, and ("baz" "bat")

Links:

Leave a comment | Trackback
Feb 27th, 2008 | Posted in Programming
No comments yet.