The most direct way to translate this into Common Lisp is:
>> x = [1 2 3 4 5]; >> x.^2 ans = [1 4 9 16 25] >>
Which leaves something to be desired, since in Matlab style code, you might have several such vectorized operations on a single line, chained. I've encountered more than one Common Lisper who is suspicious of brevity for the sake of brevity, but the major advantage of Lisp is supposed to be that you grow your language up to meet your problem. Today, at least for pedagogical purposes, our problem is we want to make Common Lisp act a bit more like Matlab. And we are going to do it with reader macros, since our major aim is to express as concisely as possible vector operations. (N.B. - I am no Common Lisp master - there may be conceptual or other errors, but the code at least works!). The first thing is to recognize the pattern we want to abstract away. A vectorial function applications looks like:
CL-USER> (defun ^ (b e) (expt b e)) ^ CL-USER> (let ((x #(1 2 3 4 5))) (map 'vector (lambda (b) (^ b 2.0)) x)) #(1.0 4.0 9.0 16.0 25.0) CL-USER>
What we are going to do is replace this with the following syntax:
(map 'vector *F* *V1* .. *VN*)
Which will mean "give me a vector produced by applying *F* to *V1* through *VN*, where *F* has arity N". The CLHS defines, with characteristic obliqueness, a Reader Macro thusly: reader macro n.
#v(*F* *V* .. *VN*)
- a textual notation introduced by dispatch on one or two characters that defines special-purpose syntax for use by the Lisp reader, and that is implemented by a reader macro function. See Section 2.2 (Reader Algorithm).
- the character or characters that introduce a reader macro; that is, a macro character or the conceptual pairing of a dispatching macro character and the character that follows it. (A reader macro is not a kind of macro.)
This tells the Reader to call VECTOR-MAP-TRANSFORMER whenever it see the pair of characters #v. All that remains is to define VECTOR-MAP-TRANSFORMER:
(set-dispatch-macro-character #\# #\v #'vector-map-transformer)
The reader expects a function with three arguments, stream, which will be bound to the current input stream, subchar, which is the subchar which triggered the call (in this case \v) and args, which we wont be using, but represent arguments passed to the Reader Macro. The body of vector-map-transformer is pretty straightforward. It extracts the function name from the sexp read from the input stream, grabs the cdr of the sexp to get the argument list, and uses back-quote notation to build a new list which will passed back to the reader. Note we use the
(defun vector-map-transformer (stream subchar arg) (let* ((sexp (read stream t)) (fname (car sexp)) (arguments (cdr sexp))) `(map 'vector (function ,fname) ,@arguments)))
functionfunction to get the function bound to
fname. This way we don't have to use a pound-quote when typing in vector functions. With these two statements, we can now invoke our reader macro at the REPL.
Sweet. Still, I find that lambda form a bit clunky. And we are always defining small lambdas to map across bodies. Let's create another Reader Macro to expand
CL-USER> #v((lambda (x) (+ x 1)) '(1 2 3 4)) #(2 3 4 5)
[x y z -> (+ x y z)]to
(lambda (x y z) (+ x y z)). In this case we are going to use the #\[ character as the trigger for our macro. And since we want to stop reading at the #\] character, rather than at an s-expression, we have to take extra care to make sure Lisp is up with everything we want. Instead of using
set-macro-characterthe major difference being that this Reader Macro is just one character, so we pass it a function with only two arguments, instead of three. There is no subchar.
(set-macro-character #\[ (lambda (stream char) (let* ((lst (read-delimited-list #\] stream t))) (short-lambda-split lst))))
short-lambda-splitdoes most of the lifting in this Macro. Note, though, that instead of using
read-delimited-listand terminate the read on the character #\]. This gives us the body of the short lambda form.
short-lambda-splitthen converts this into the sexp to return to the reader:
This function basically takes the list, finds the
(defun short-lambda-split (elements) (let ((args (loop while (and (not (eq (car elements) '->)) elements) collect (prog1 (car elements) (setf elements (cdr elements)))))) (if (not elements) ; There is no arg list `(lambda (x) ,@args) `(lambda (,@args) ,@(cdr elements)))))
->symbol which separates the argument list from the body and then constructs the lambda expression. If there is no
->the function assumes one argument, named
xand that the entire list passed in represents the body of the function. We can write an arity one short lambda like
[(+ x 1)]without specifying the lambda argument list. Before we can use the short-lambda reader macro we have to do something about the
#\]characters polluting the input stream or they will confuse the reader. The solution is to tell lisp to treat them like
#\)(closing parenthesis) characters so that it won't complain (at least I believe this is what is going on). We do that this way:
We can now use our new short lambda in conjunction with our vector application operator to write some very concise code for vector operations.
(set-macro-character #\] (get-macro-character #\)))
A word of caution: Macros are always tricky beasts, and extending Lisp this way may only seem like a good idea. In practice, it introduces all sorts of tricky issues. In addition to the regular issues of macro expansion, most of which apply more or less to reader macros, adding new forms to the language confuses tools which might be trying to understand Lisp syntax, like Emacs. I also find that my Lisp (SBCL) can sometimes return confusing errors when reader Macros are in use. So use this information wisely. I nevertheless hope this little tutorial has helped clarify a very interesting ability of Common Lisp. Using these macros, vectorized numerical code is much shorter and easier to understand. I used these macros to write some code for a class I TA where Matlab is the language of choice. Most of the time, these simple extensions put the Common Lisp implementation on the same footing, brevity-wise, as Matlab and I got to program in Lisp, which is a lot more fun. Comments and questions welcome. PS - I apologize for any confusing indentation - Blogger is being very disrespectful with my
CL-USER> (let ((x #(1 2 3 4 5))) #v([b -> (^ b 2.0)] x)) #(1.0 4.0 9.0 16.0 25.0) CL-USER>