random-state.net

Nikodemus Siivola

<< next | top | previous >>

Experiments in Fun[ctional] Programming #
hacking, January 20th 2008

WARNING: May Contain Trace Elements of Parody

I mentioned the possibility to use <- instead of #L a while ago. Then I got to talking with Troels Henriksen of the Climacs (which is Emacs of tomorrow -- like Arc is to Common Lisp, except that it actually exists!) and McCLIM fame about related matters. Like so many others, I've always been intrigued by the utter clarity functional programming delivers -- on thing led to another, and now I'd like to introduce you to my three new best friends: <-, <---, and --->.

The core idea is to be able to express lazy and partial evaluation succintly -- because, as we all know, terseness is a virtue: code that is never written is the best code. Naysayers will claim that overly terse code can be hard to understand, but as long as there is less of it, that really doesn't count, does it?

Firstly, we have the idea of "streams" or "pipes": functions called repeatedly to produce different values: such a function can easily express eg. any infinite series. Of course, for this to to be of any utility, we need a utility to do this calling in a terse-yet-expressive manner. This is what ---> does:

(defmacro ---> (size composition &rest arguments)
  (let ((express (gensym "EXPRESS"))
        (unexpressed (gensym "UNEXPRESSED"))
        (expressed (gensym "EXPRESSED"))
        (expression-arguments (gensym "EXPRESSION-ARGUMENTS")))
    `(let ((,expression-arguments (list ,@arguments)))
       (labels ((,express (,expressed ,unexpressed)
                  (if (zerop ,unexpressed)
                      ,expressed
                      (,express (append ,expressed (list (apply ,composition ,expression-arguments))) 
                                (- ,unexpressed 1)))))
         (,express nil ,size)))))

This macro expands into an elegant recursion that constructs a list by repeatedly calling a function with the results of evaluating arguments. The last bit is the key to its power:

;;; no need for this
(let ((stream (make-stream))) (---> *n* (composition) stream))

;;; just this is enough
(---> *n* (composition) (make-stream))

Next, we need tools for writing streams. What we would like to be able to do is:

(defun natural-number-stream () (<--- 0 1+))

Happily, <--- is simple enough:

(defmacro <--- (start step)
  (let ((value (gensym "VALUE")))
    `(let ((,value ,start))
       (lambda () (prog1 ,value (setf ,value (,step ,value)))))))

Finally, we want to be able to express our composition nicely. Say, we would like to put (lambda (x) (- (funcall x)) in a better way.

;;; This would be nice: no extra parens!
(<- -)

Again, easy enough. This is a slight extension to my earlier <-, which deals with the _ implicitly, and automatically handles functions as arguments, so that it can be used with streams like above.

(defmacro <- (operation &rest arguments)
  (let ((processed-arguments (if (member '_ arguments) arguments (append arguments '(_)))))
    `(lambda (_) 
       (setf _ (if (functionp _) (funcall _) _))
       (,operation ,@processed-arguments))))

That's all there is to it! Now we can express mappings over natural numbers in a clear and concise manner:

(---> 3 (<- -) (<--- 0 1+)) ; => (0 -1 -2)

Where are <-- and --> you might ask, naturally enough. I'm reserving them for implementing general purpose lazy evaluation...