Search code examples
clojurelispcommon-lispmetaprogramminghomoiconicity

Is Clojure less homoiconic than other lisps?


A claim that I recall being repeated in the Clojure for Lisp Programmers videos is that a great weakness of the earlier Lisps, particularly Common Lisp, is that too much is married to the list structure of Lisps, particularly cons cells. You can find one occurrence of this claim just at the 25 minute mark in the linked video, but I'm sure that I can remember hearing it elsewhere in the series. Importantly, at that same point in the video, we see this slide, showing us that Clojure has many other data structures than just the old school Lispy lists: enter image description here This troubles me. My knowledge of Lisp is quite limited, but I've always been told that a key element of its legendary metaprogrmmability is that everything - yes, everything - is a list and that this is what prevents the sort of errors that we get when trying to metaprogram other languages. Does this suggest that by adding new first-class data structures, Clojure has reduces its homoiconicity, thus making it more difficult to metaprogram than other Lisps, such as Common Lisp?


Solution

  • is that everything - yes, everything - is a list

    that has never been true for Lisp

    CL-USER 1 > (defun what-is-it? (thing)
                  (format t "~%~s is of type ~a.~%" thing (type-of thing))
                  (format t "It is ~:[not ~;~]a list.~%" (listp thing))
                  (values))
    WHAT-IS-IT?
    
    CL-USER 2 > (what-is-it? "hello world")
    
    "hello world" is of type SIMPLE-TEXT-STRING.
    It is not a list.
    
    CL-USER 3 > (what-is-it? #2a((0 1) (2 3)))
    
    #2A((0 1) (2 3)) is of type (SIMPLE-ARRAY T (2 2)).
    It is not a list.
    
    CL-USER 4 > (defstruct foo bar baz)
    FOO
    
    CL-USER 5 > (what-is-it? #S(foo :bar oops :baz zoom))
    
    #S(FOO :BAR OOPS :BAZ ZOOM) is of type FOO.
    It is not a list.
    
    CL-USER 6 > (what-is-it? 23749287349723/840283423)
    
    23749287349723/840283423 is of type RATIO.
    It is not a list.
    

    and because Lisp is a programmable programming language, we can add external representations for non-list data types:

    Add a primitive notation for a FRAME class.

    CL-USER 10 > (defclass frame () (slots))
    #<STANDARD-CLASS FRAME 4210359BEB>
    

    The printer:

    CL-USER 11 > (defmethod print-object ((o frame) stream)
                   (format stream "[~{~A~^ ~}]"
                           (when (and (slot-boundp o 'slots)
                                      (slot-value o 'slots))
                             (slot-value o 'slots))))
    #<STANDARD-METHOD PRINT-OBJECT NIL (FRAME T) 40200011C3>
    

    The reader:

    CL-USER 12 > (set-macro-character
                   #\[
                   (lambda (stream char)
                     (let ((slots (read-delimited-list #\] stream))
                           (o (make-instance 'frame)))
                       (when slots
                         (setf (slot-value o 'slots) slots))
                       o)))
    T
    
    CL-USER 13 > (set-syntax-from-char #\] #\))
    T
    

    Now we can read/print these objects:

    CL-USER 14 > [a b]
    [A B]
    
    CL-USER 15 > (what-is-it? [a b])
    
    [A B] is of type FRAME.
    It is not a list.