Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I used to like Lisp's homoiconicity. These days, I'm not even sure the concept makes any sense? What is it even supposed to mean?

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.)



> Eg no modern Lisp stores a function as an S-Expr of its syntax

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.


Of course, compilers exist, so in a sense all code is data.

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)


Well sure, if you hand wave away the main benefit of sexps then there aren’t many benefits.


Oh, S-Expressions are great for certain kinds of things.

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.


Have you ever written deep macros or only surface level ones?


What do you mean by 'deep' vs 'surface level' macros?

Do you mean something like implementing Prolog with macros in Lisp as a 'deep' macro? Or something else?


Yes


I have worked with overly complicated macros. What about them?

They aren't magic.


Maybe you can write a detailed blog post that compares making a DSL in lisp and a language of your choice. I would love to read it


> I used to like Lisp's homoiconicity. These days, I'm not even sure the concept makes any sense? What is it even supposed to mean?

You are not alone:

http://calculist.org/blog/2012/04/17/homoiconicity-isnt-the-...


Thanks! That blog post expresses much more clearly what I am trying to say.

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.


> Parentheses make it unambiguous for the expander to understand what the arguments to a macro are, because it’s always clear where the arguments begin and end. It knows this without needing to understand anything about what the macro definition is going to do. Imagine trying to define a macro expander for a language with syntax like JavaScript’s.

Are you sure this blog post is making the point you have been trying to make?

Here is blog post that explores writing C in s-expressions.

https://itnext.io/what-makes-lisp-unique-8a0576b42293


I can't imagine someone who has ever written a Lisp macro coming away thinking "there's nothing special about s-expressions". Honestly, sounds like you're commenting on something you have absolutely no knowledge of.


Great ad hominem!

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.


> I know what I am talking about.

Sorry, but you manifestly don't:

> there's no magic to them (compared to macros you could do as pre-processing in any other language)

The "magic" of s-expressions is that they make it easy to operate on the source code of a program as a hierarchical data structure (i.e. as an AST) rather than as text. That turns out to be an extremely powerful lever. It is is one of the reasons Lisp has lasted as long and been as influential as it has. I'm sorry if this sounds like an ad-hominem, but if you think there is "no magic" to S-expressions the most likely explanation is that you don't really understand them. There is nothing comparable in any other language. There's a reason that they are still in use today. Indeed, there is a reason that they keep getting re-invented again and again. HTML, XML, and JSON are all (badly) reinvented s-expressions.

> homoiconicity is a weird concept

No, it isn't. It is simple and straightforward: homoiconicity is where the primary representation of programs is a data structure in a primitive type of the language itself. It isn't any weirder than (say) recursion. If you think it's weird that is more evidence that you don't actually understand it.


But Rust macros also do this, the procedural macros operate on the AST of the macro

https://doc.rust-lang.org/reference/procedural-macros.html


Plenty of languages give you the option of writing macros, however the difference between writing macros in a lisp and those languages is like the difference between using a general purpose programming language and an accidentally Turing-complete one.


C's preprocessor is such an 'accident'. But eg Rust's macros and template Haskell are very deliberately designed.


No. You can say that Common Lisp and Racket are deliberately designed around macros. In Haskell, Rust, C++, they have been bolted on to the language as an after thought. Unless I am mistaken, those languages do not have the ability to write DSLs interactively, unlike Common Lisp.


What do you mean by 'write DSLs interactively'?


That's not how Lisp works. Lisp macros don't operate on an AST.


I don't know how they work, how do they work?


Macros get and produce unparsed Lisp code, as nested lists.


>The "magic" of s-expressions is that they make it easy to operate on the source code of a program as a hierarchical data structure

where exactly is the magic? it's just tuple unpacking? like congrats you've constrained yourself to the absolute bare minimum and you've made it work. i mean i guess congrats for getting it to work at all but it's not magical, it's tedious. if you showed me a homoiconic language that did somehow pull off the magic trick of being more expressive than tuples then i would be indeed enchanted (mathematica at least manages to put lipstick on the whole exercise).


> i mean i guess congrats for getting it to work at all but it's not magical, it's tedious.

Magic lies in the eyes of the beholder. So you may not find it magical while I do. But I don't understand how you say that it is tedious.

With the homoiconicity of Lisp, you use the Lisp language itself to write macros and you manipulate the lists that make your Lisp programs directly.

Mainstream languages these days either need you to learn a special syntax for macros or they need you to learn their AST structures/classes and manipulate them or sometimes both. Isn't this more tedious? Isn't the Lisp way simpler and less tedious?


> where exactly is the magic?

There is a reason I put the word "magic" in scare quotes.

> it's just tuple unpacking?

It's not even that. S-expressions are just a serialization of linked lists. The "magic" happens because the details of the serialization happen to make them particularly good for writing code.

See my reply to /u/eru for more details. https://news.ycombinator.com/item?id=36599470


> It's not even that. S-expressions are just a serialization of linked lists.

Well, it's more like serialization of a binary tree. Cons-cells can branch in both the car and the cdr direction.


The distinction between a linked list and a binary tree is in the eye of the beholder. One of the brilliant things about s-expression syntax is that (a b c) is equivalent to (a . (b . (c . nil)))) so there is no distinction between binary trees and linked lists (whose elements can be other linked lists) other than that to be a "proper" list it has to be null-terminated.


> The "magic" of s-expressions is that they make it easy to operate on the source code of a program as a hierarchical data structure (i.e. as an AST) rather than as text.

As https://news.ycombinator.com/item?id=36597550 points out, you can do this with more complicated syntax as well. It's just a bit more annoying.

I agree that ergonomics are a big deal when using programming languages, and it shapes their culture.

Just like it's a pain to re-use code in C, so everyone always re-implements linked lists from scratch every time they need one.

> It is simple and straightforward: homoiconicity is where the primary representation of programs is a data structure in a primitive type of the language itself.

Why does it have to be a 'primitive' type?

Eg Haskell supports representing ASTs just fine, but you would use user-defined types for that.

Imagine a variant of Haskell that used S-Expression syntax for the sake of argument. That version of Haskell would still represent S-Expressions internally with a user-defined type. See https://hackage.haskell.org/package/s-expression and mentally translate all the code into S-Expressions (but leave the semantics the same).

Or just imagine a variant of Lisp where cons-cells are a user-defined data-structure. That wouldn't make their S-Expressions any worse, would it?

As long as whatever data structure you use to represent your AST is easy to work with, that's surely good enough?

And what do you mean by 'primary' representation? A Lisp compiler (or interpreter) has many different layers, and the AST is but one representation used in one of the layers.

For many purposes, S-Expressions aren't particularly useful, because eg they don't keep track of which variables are bound where. And, of course, an interpreter that works by walking S-Expressions is really, really slow.

So in what sense are S-Expression a primary representation?

You could imagine a re-skin of C that used S-Expression syntax. It's actually relatively easy to write such a front-end as a pre-processor that translates S-Expression syntax into classic C-syntax before feeding the result to a C compiler. (And allows you to run C programs on your S-Expressions as macros.)

But that change by itself wouldn't change too much about the language. And wouldn't make S-Expression-C as pleasant a language as any old Lisp.


> It's just a bit more annoying.

Well, yeah, but that is missing the point rather badly. There is nothing you can do in any high level language that you can't do in assembler. The only reason high level languages exist at all is to make programming less annoying.

> For many purposes, S-Expressions aren't particularly useful, because eg they don't keep track of which variables are bound where.

S-expressions are nothing more than a serialization of binary trees. There is a long, long list of features that they do not provide. Again, if you think that is relevant, you have completely missed the point.

For the record: the point, the thing that makes s-expressions cool, is that they provide a serialization of linked lists, and this serialization is super-simple, to the point where writing code in it is actually practical. They weren't designed for this. The fact that you can actually write practical code in s-expressions was a surprise, a discovery, not an invention. You can serialize binary trees as XML or JSON, and you can even use those to write code if you want to, but no one in their right might would actually do that. You would go nuts typing all those angle brackets, double-quotes, and commas. The reason s-expressions are cool is that they are parsimonious. You don't need all the angle-brackets and quotes and commas. S-expressions are actually a reasonable syntax for writing code whereas XML and JSON are not despite being fundamentally the same thing under the hood. Indeed, once you get accustomed to s-expressions, they are a superior syntax for writing code than conventional languages because they are easy to parse (no precedence rules to deal with).


> Well, yeah, but that is missing the point rather badly. There is nothing you can do in any high level language that you can't do in assembler. The only reason high level languages exist at all is to make programming less annoying.

Oh, there's plenty of stuff you deliberately _can't_ do in high level languages (compared to assembly or low level languages).

Eg Python doesn't let you use naked jumps (also called GOTO). Haskell doesn't let you mutate state willy-nilly.

> You don't need all the angle-brackets and quotes and commas.

Well, you have quoting and quasi-quoting. Very useful, and they use some more interesting syntax.

Yes, S-Expressions are a reasonable syntax. I used to like them a lot. But I also came to appreciate more traditional syntax when switching from Scheme to Haskell.

I was not arguing against S-Expressions at all. Only against 'homoiconicity' being a coherent or useful concept, or saying anything deeper about a language.

As I said earlier, if you start from C, change its syntax to use S-Expressions (rather simple to do), and allow S-Expression based macros (that use the same C-as-S-Expression syntax), you still don't have a language that's as pleasant to work with nor makes macros as easy as your favourite Lisp.


> Python doesn't let you use naked jumps

Not natively, but you can do it. It's just "a bit more annoying".

> you have quoting and quasi-quoting

That is obviously not what I meant. What I meant was that you can write (x y z) instead of ("x", "y", "z"). The latter is just "a bit more annoying" but that "bit" makes all the difference.

Also, ' and ` are not really a core part of s-expression syntax, they are just a convenient shorthand. Any s-expression written using ' and ` can also be written without them. The core syntax includes only parens and dots, and of course the constituents of atoms. Modern implementations also include native support for double-quoted strings, but that was not part of the original spec. Everything else is just syntactic sugar.


s-expressions can provide a hierarchical structure, but they are not representing AST. It's just another form of source code.

> And, of course, an interpreter that works by walking S-Expressions is really, really slow.

Common Lisp interpreters work by walking S-expressions. Typically you'll see the s-expressions directly in a debugger -> modifying the s-expression will modify the running program.


> 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.

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?


Sorry, I should have been more precise.

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.)


Yes, I'm having trouble getting precisely to what you are talking about. Do you mean something like SBCL compiling to machine code, for example? If so, how does that affect using Common Lisp to implement some DSL, in the sense that it takes away the power of s-expressions for achieving this?


I am saying that S-Expression aren't any kind of 'primary' representation of your program in a modern Lisp. Especially a compiled one.

S-Expressions (or more precisely, the AST) is just one representation amongst many that's in use during the different stages of the compilation process.

They are still a great syntax choice for the kind of languages that Lisps are. Totally agreed.

My point was that homoiconicity is a surface level property that you could bolt on to eg C or Cobol, via a simple pre-processor, and it wouldn't change those languages much.

Just like you could bolt on using Python-style significant indentation to C or Fortran or Lisp, and it wouldn't change those languages much. (Apart from changing their syntax, of course.)


> I am saying that S-Expression aren't any kind of 'primary' representation of your program in a modern Lisp. Especially a compiled one.

Besides pointing out like others have that pretty much all Lisps have an interpreter that stores programs as s-expressions, why do you think compilation makes making a DSL in Lisp harder? Do you think you somehow need a parser all of a sudden?

> My point was that homoiconicity is a surface level property that you could bolt on to eg C or Cobol, via a simple pre-processor, and it wouldn't change those languages much.

Writing in C instead of assembly does not make your processor more powerful. Similarly, when designing languages, homoiconicity allows you to work directly with the AST in a way that is most ergonomically simple. Saying this is a surface level thing is an understatement


The nested list objects, whose printed representation is called S-expression, are the representation of the source code of Lisp. This is important and primary in multiple senses of the word.

In version control jargon, only primary objects should (ideally) be tracked in a repository, not secondary/derived ones. The most common example of primary objects are source code files; and of secondary/derived objects, compiled files.

Programs first exist as source code; primary means first. The semantics of the language is defined with reference to the syntax, whose description refers to the source code form. Everything begins with the semantics: it is primary. Other forms, like compiled, have to produce the same results like the reference evaluation model over the source code. Expansion of macros is defined over the source code. Other tooling is based on source code, like text editing, tags generation and whatnot.

Compiled code is not primary in any way. It is non-portable, sometimes not even fully documented, and supports almost no useful operations beyond invoking it (calling a compiled function or loading a compiled file) and disassembling compiled code.

The representation of compiled code can change between releases of a Lisp; the worst that happens is that users have to recompile their code since old compiled files do not load any more. If you break the source language, there will be gnashing of teeth.

The basis of portability among implementations of the same dialect is always the source code.

Lisp programs can quote pieces of themselves as literal constants. Atoms can be quoted, as well as symbols and entire lists. These quoted lists have to survive into the compiled form. Thus, the syntax is important because any piece of it can potentially be a literal which has to retain its shape and content no matter how the program is translated.

Textual source code contains valuable information not found elsewhere: formatting and comments.

In Lisps that have programmable readers, like Common Lisp, only the source code contains instances of the original read syntax. For instance, in some implementations of the backquote syntax, the reader expands it, and this may be done in such a way that the resulting object cannot be printed as backquote syntax any more. Application-defined read syntaxes usually do not support printing back in the original form. Thus they appear only in the textual source code.

Primary information is already lost when we have merely read an expression into memory without doing anything else, like expanding and compiling it.


actually: Common Lisp interpreters do walk s-expressions




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: