Is Clojure less homoiconic than other lisps? [closed]
Asked Answered
F

4

8

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?

Forsyth answered 18/1, 2021 at 20:24 Comment(5)
This is the canonical example of an opinion-based question. It should be closed as such: it's a good question, but it's not suitable for SO.Unweighed
@tfb Do you not believe homoiconicity to be a measurable quantity?Forsyth
Homoiconicity is a single bit: either a language's source code can be processed as a (predefined) data structure in that language, or it can't. Everything else is opinion.Unweighed
Clojure is quite far from a Lisp; it is a domain specific language for manipulating immutable, persistent sequences, inspired by Lisp syntax and a substantially revised conception of its metaprogramming concepts.Ironwood
The only requirement for metaprogramming was always an ability to construct ASTs in compile time. AST can be anything, not necessarily a mere pile of cons cells. Scheme "syntax objects" are not lists at all, for example. You can convert them to lists and back, but it's not a good idea as you're losing location metadata.Walleye
B
13

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.
Britzka answered 18/1, 2021 at 21:43 Comment(1)
"Everything in Lisp is a list" is actually even wronger than the similarly-popular "everything in Haskell is a function".Simonne
S
10

Let's break the word "homoiconicity" apart.

  • homo-, meaning "same"
  • iconic, here meaning something like "represented by"
  • -ity, meaning "something having such a property"

The quality that makes macros possible (or at least much easier) is that the language itself is represented with the same data structures that you use to represent other objects. Note that the word is not "monolistism", or "everything is a list". Since Clojure uses the same data structures to describe its syntax as it does to describe other values at runtime, there's no excuse to claim it is not homoiconic.

Funnily enough, I might say that one of the ways that Clojure is least homoiconic is a feature that it actually shares with older lisps: it uses symbols to represent source code. In other lisps, this is very natural, because symbols are used for lots of other things. Clojure has keywords, though, which are used much more often to give names to data at runtime. Symbols are quite rarely used; I would go so far as to say that their primary usage is for representing source code elements! But all of the other features Clojure uses to represent source code elements are frequently used to represent other things as well: sequences, vectors, maps, strings, numbers, keywords, occasionally sets, and probably some other niche stuff I'm not thinking of now.

Simonne answered 18/1, 2021 at 20:35 Comment(3)
So the fewer representations for data structures and objects, the easier it is to write macros. And Clojure's unique not just because it uses symbols, but because it works with both symbols and keywords. Clojure uses the same symbols for source code as commonly coded data structures. Do I understand this right? I've only used macros (just got the "aha" for ->> yesterday), but I'm excited for the day when I learn to write my own :)Hynda
I don't think "the fewest" is a good takeaway. There's a reason nobody writes real programs (macros or not) in lambda calculus. The important thing is that source code is represented in a format that is easy to consume and produce if you are used to the non-macro part of the language.Simonne
If you take all of the basic construction mechanisms of a language and list them, then count how many of those constructs are homoiconic, then divide that number by the total number of items in the list, then you would have a percent that describes the homoiconicity of the language, its feature-homoiconicity, if you will. That is what I think this question was trying to ask. in other words, if there are 100 language features in a prefix-Lisp, and one of those is infix integer expressions, then that language would be max 99% homoiconic.Starstarboard
P
2

In Clojure, the only one of those extra data structures that is required for some language constructs, besides lists, are vectors, and those are in well-known places such as around sequences of arguments to a function, or in symbol/expression pairs of a let. All of them can be used for data literals, but data literals are less often something you want to involve when writing a Clojure macro, as compared to function calls and macro invocations, which are always in lists.

I am not aware of anything in Clojure that makes writing macros more difficult than in Common Lisp, and there are a few features there distinct to Clojure that can make it a little bit easier, such as the behavior where Clojure backquoted expressions by default will namespace-qualify symbols, which is often what you want to prevent 'capturing' a name accidentally.

Pyrochemical answered 18/1, 2021 at 20:34 Comment(1)
FWIW keywords and maps are regularly needed for language constructs, most prominently metadata.Simonne
H
2

As previous answerers pointed out, "homoiconicity" means nothing else than that the famous "code is data": That the code of a programming language consists 100% of data structures of that language, thus is easily build-able and manipulateable with the functions in the language usually used to manipulate the data structures.

Since Clojure data structures include lists, vectors, maps ... (those you listed) and since Clojure code is made of lists, vectors, maps ... Clojure code is homoiconic.

The same is true for Common Lisp and other lisps.

Common Lisp contains also lists, vectors, hash-tables etc. . However, the code of Common Lisp sticks more rigidly to lists than Clojure does (function arguments are in a argument list and not in a vector like in Clojure). So you can more consistently use functions to manipulate lists for metaprogramming purposes (macros).

Whether this is "more" homoiconic than Clojure is a philosophical question. But definitely you need a smaller set of functions for manipulation of the code than in Clojure.

Hernadez answered 21/1, 2021 at 9:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.