Lisp has (simple) syntax for creating literals and writing code. But when you inspect your data from the inside of Lisp, they don't have any more (or less) relation to that syntax than eg Python does.
Eg no modern Lisp stores a function as an S-Expr of its syntax. You get an opaque piece of data that you can do certain operations on, just like in eg Python or Haskell.
Lisp using S-Expressions is an interesting syntax choice, but that's it. It doesn't by itself have any deeper impact on the language. (Of course, socially it does have a deeper impact, because it makes it simple for language users to write macros that look exactly like built-in parts of the language, instead of bolted on.
But that's all in human brains and social interactions, nothing fancy going on here on a technical level.
If you wouldn't mind much more tediousness, you could implement seamless macros in almost any language as a pre-processing step.)
Many Common Lisp implementations do that, especially those with an interpreter: SBCL (yes, SBCL has an interpreter, too -> one can tell SBCL to use an interpreter), LispWorks, a Lisp Machine, Allegro CL, CLISP, ...
LispWorks:
CL-USER 18 > (defun foo (a b) (* a (let ((a (* a a))) (break) (+ b (expt a b)))))
FOO
CL-USER 19 > (foo 1 2)
Break.
1 (continue) Return from break.
2 (abort) Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 20 : 1 > :n
Call to INVOKE-DEBUGGER
CL-USER 21 : 1 > :n
Call to BREAK
CL-USER 22 : 1 > :n
Interpreted call to FOO
You'll see that LispWorks says that FOO is running using the s-expression interpreter. Next we'll have a look at this s-expression: CL-USER 23 : 1 > :lambda
(LAMBDA (A B) (DECLARE (SYSTEM::SOURCE-LEVEL #<EQ Hash Table{0} 82200D6A93>)) (DECLARE (LAMBDA-NAME FOO)) (* A (LET (#) (BREAK) (+ B #))))
The list is a value, which we can pretty print: CL-USER 24 : 1 > (pprint *)
(LAMBDA (A B)
(DECLARE (SYSTEM::SOURCE-LEVEL #<EQ Hash Table{0} 82200D6A93>))
(DECLARE (LAMBDA-NAME FOO))
(* A (LET (#) (BREAK) (+ B #))))
If you would modify this s-expression in the debugger, the program is directly changed.Besides the social stuff, S-expressions make it orders of magnitude simpler to build the infrastructure required to support seamless macros (just look at procedural macros in Rust for a recent example)
I'm just having a problem with the concept of homoiconicity being touted as both coherent, and something that goes deeper than the very surface syntax level.
You are not alone:
http://calculist.org/blog/2012/04/17/homoiconicity-isnt-the-...
Btw, it seems you could implement a 'read' that also recognises Python/Haskell-style significant indentation in addition to parens only. That would be an interesting elaboration of the point in the post.
I have used Scheme, Racket and Common Lisp (SBCL) since the 1990s (before mostly switching to the ML family of languages). I read 'On Lisp' cover to cover. I know what I am talking about. You can still disagree with me, of course. But don't do it of the basis of me not having used enough Lisp.
S-Expressions make it easier to write macros that blend seamlessly into the host language. But there's no magic to them (compared to macros you could do as pre-processing in any other language) beyond that.
S-Expressions are a neat syntax. But homoiconicity is a weird concept; if it is coherent at all, it only pertains to the surface syntax level of the language.
In Common Lisp
(defun times-two (x) (\* x 2))
(print (function-lambda-expression #'times-two))
> (LAMBDA (X) (BLOCK TIMES-TWO (* X 2)))
What would you do in Python or Haskell?Yes, Common Lisp stores the s-expression. And you can get at it by applying an operation (in your case, `function-lambda-expression`) to some opaque handle.
The function itself isn't stored as its s-expression. Neither does any reasonable interpreter work by walking the s-expression. (Doing so would be very slow.)
Forth also has an interesting simple syntax.