Do you mean Scheme, specifically? Because I have a hard time seeing that in lisp in general. Racket's small but fairly pragmatic, Clojure is pretty big, and Common Lisp is frankly a bit of a kitchen sink language.
Not your parent, but I personally didn't find S-expression syntax to be superior. Treating code as data and vice versa is metaprogramming, and it should be something you know how to do when you need it, easy enough to where you're not really going out of your way to do it when you do need it, but weird enough so that you can notice it and immediately see that you are going to need to put on your thinking cap in order to proceed further down the road of understanding this program, when you come across it.
Amusingly, I've yet to hit as much frustration as I have when using a few ruby packages at work. Holy hell is it confusing to figure out where things came from, sometimes.
I don't necessarily care for the data as code, most of the time; but I do like that most everything has a sensible way to serialize to a file. Even better, unless I am wanting to get optimized, the round trip can take care of itself.
Curious what the syntax is for a basic list of key value pairs? Well, it is the same as any list of pairs. Which is the same as any list, unless you are trying to get fancy.
This freedom really is liberating, at times. Yes, some folks have made macros so that they have a syntax to have a different literal for different styles of maps/tables/etc. By and large, life is easier if you don't use those and just stick to the minimal syntax necessary to do this.
Which really helps with your concern. When I can see what the transformation is, I can literally substitute into my mental model what the macro is doing, so that I don't have to juggle a bunch of crap around.
And all of this, oddly enough, lands you into not caring if you are calling to a function or a macro. I have more trust in calling things in most lisps I've worked with, because I can understand both ways in terms of themselves easily enough. With ruby, I'm often wary of using a new method in a source file, because I really can't trust all of the interactions that are assumed for it to work out.
I'm not sure how much of our respective experiences is due to familiarity. For example, I have REPL power tools and a really good understanding of Ruby's object model to help me figure out what something's doing. So that almost never concerns me all that much.
Whenever I pull in a new gem, I usually budget a few minutes to wrap my head around the semantics of how the gem is expected to be used. Occasionally the documentation isn't good enough and I need to source dive.
What I never have to deal with is new syntax. No matter how Ruby is being used, the syntax is always the same comforting, pleasing-to-read near-prose. What changes when you use metaprogramming in Ruby is semantics. You can't add new syntax to the language unless you actually change the language.
Like you, I find people misusing metaprogramming. For me usually this is fairly easy to detect. If I look for the source of a method and it's dynamically generated, then showing the source will take me to where it was generated.
I won't pretend to have chosen Ruby over Lisp because I liked the language better. I chose it because I figured it would help me get a job better. But as time went on I found myself at home with Ruby.
I don't think it would take me all that long to get used to any given Lisp. But I don't believe it actually gives anyone superpowers, at least, not superpowers you can't have with Ruby. I have yet to hear a lisper actually articulate a solid advantage for code as data.
When there isn't any functional reason to choose between alternatives, all there's left is the aesthetic. Which is where all those parenthesis finally become important.
Hmm, you seem to be indicating that people use macros to create new syntax all of the time. I have not actually found that to be the case that often. In particular, there are only two macros I know I use somewhat regularly that do this. LOOP and FORMAT. And even those always follow the same general syntax of lisp. Unless you get crazy, it is not usual to actually use things that drastically change the full syntax of lisp.
But again, in lisp, you probably make use of more metaprogramming than you'd realize, precisely because it is indistinguishable from normal programming.
(I'm tempted to contrast this with some of the DSLs I've seen in the likes of scala and ruby... I have a hypothesis that all of the hoops you have to jump through to make a macro like construct cause people to go all in when they do so.)
Now, I'm also not claiming lisp gives superpowers. Again, I'd almost go the other route. Most lisp is typically quite simple, because you can accomplish quite a lot that way.
Which does bring it back to aesthetics some. The parens just flat out don't bother me. They are a bemusing joke, that never actually made sense. Too often, the difference is minimal compared to most C programs. Foo(bar) becomes (Foo bar). Which isn't that big of a deal. The basic math changes from infix to prefix is a bit to get used to. However, I find it is much more pleasant to deal with generics (+ matrix1 matrix2) as opposed to matrix1.plus(matrix2) of java and the like. Of course, I was also a huge fan of my HP calculator in college. RPN for the win! :)
So, yes, I do think it comes down to preference. I don't think you're preference is wrong/bad. I do think your view of lisp sounds colored more by bemusing jokes than actual problems in lisp code. But, I fully grant my experience with ruby may be colored by an overly ambitious project written in it.
Indeed, apologies for messing that up. :) My point was more on the rarity of "syntax breaking" macros. And, even that one doesn't really change the syntax much. It is just a very complicated function, for all intents and purposes.
Not speaking for grandparent, I would say that cond isn't syntax-breaking because it doesn't have to employ grammar -driven parsing to recognize and delimit the clauses and their constituent forms. The nested list structure of cond that you see is the real syntax.
defun could be like this, though its ANSI CL specification is quite screwed up.
where the double square brackets are described in "1.4.1.2.1 Splicing in Modified BNF Syntax" which denotes that a list of elements is to be spliced into a larger structure, and the elements may appear in any order. The description of this idiotic bullshit is convoluted, but an example section gives the gist of just how idiotic:
"For example, the expression
(x [[A | B* | C]] y)
means that at most one A, any number of B's, and at most one C can occur in any order. It is a description of any of these:
(x y)
(x B A C y)
(x A B B B B B C y)
(x C B A B B B y)
but not any of these:
(x B B A A C C y)
(x C B C y)
Note how the B elements generated by B* can be interspersed! So in the case of defun it means that we can have variations like:
(defun name (args ...) decl doc decl decl decl body...)
(defun name (args ...) doc decl decl decl decl body...)
(defun name (args ...) decl decl decl decl doc body...)
There can be at most one docstring, and an arbitrary number of declarations, and they can be mixed in any order.
Note that if we interpret A|B*|C as a simple regex notation, it wants all the B's clumped together; someone really had to work hard to concoct this counterintuitive BNF extension.
That all said, I can't imagine that most folks are surprised by the general shape of most defun statements. The complexities that go into how it is implemented is crazy, but it is not at all like the first time you see a LOOP invocation, is it?
> I can't imagine that most folks are surprised by the general shape of most
defun statements.
The general shape of a LOOP expression is just a list of symbols and lists. A
complicated LOOP expression that uses many features at once doesn't end up
looking very Lispy, but a shorter one like (loop :repeat 10 :do (print
'hello)) shouldn't look at all foreign, especially if you write the keywords
as keywords, which many people do.
I guess when people talk about the regularity of Lisp syntax, I think they
usually mean that (op ...args) applies the operator OP to the list ARGS. LOOP
obeys this very well: the only LOOP syntax I can think of that breaks it is
`using (hash-key k)` or with HASH-VALUE. Universally accepted macros break
this rule all the time, though. By shoving everything into lists, they totally
abandon trying to keep with the (op ...args) form. If you see that regularity
as a great advantage of Lisp's, then it seems like you should be much more
bothered by the more "mundane" macros than you should be by LOOP.
Commonly used loop syntax is replete with syntax that breaks "op args ...":
when <condition> do
for <var> in <list>
for <var0> = <init0> then <step0> and
for <var1> = <init1> then <step1>
collecting <expr> into <var>
The normal macros that don't have a flat argument structure, but shove everything into lists are keeping with the idea of leveraging the nesting so that the construct is "parsed on arrival" (POA?) into the macro. All that remains is to access its structure.
Syntactically, those are things that could have been implemented as operators, but aren't, they're just LOOP keywords; lots of functions take keyword arguments (edit: even if LOOP's are a little different from &key parameters). LOOP avoids using unquoted lists for things other than calling operators.
edit: Oh, right, destructuring, too! I guess that also counts.
Out of curiosity, how would you feel about a macro like this (ignoring that it
doesn't enforce any keyword order)?
(defmacro for (var &key = (then =) while do)
`(do ((,var ,= ,then))
((not ,while))
,do))
I was taking "syntax" to be "ubiquitous syntax". To that end, those don't really pose a big surprise.
Cond might. But really, if I said to prepare a list of test to action pairs, it has the somewhat obvious syntax to execute the first one with a true test.
Pcond is a bit crazier. But you can go a long way without ever touching that one.
Defclass is powerful, though again, you can go a long way without it. And if you are in a codebase using it, it is probably a touch on the ubiquitous side.
Loop is just famous for having a ridiculously powerful language inside it. Much more so than the others, which, quite frankly look like normal function calls most of the time.
> (I'm tempted to contrast this with some of the DSLs I've seen in the likes of scala and ruby... I have a hypothesis that all of the hoops you have to jump through to make a macro like construct cause people to go all in when they do so.)
I don't think we have any points of contention left to argue over, but I will say that many Rubyists just aren't familiar enough with Ruby's object model and scoping to be able to do DSLs cleanly. It's certainly possible, but there's a learning curve.
Actually I can articulate how to do DSLs in a paragraph. Create a DSL class where all instance methods are DSL calls that set instance variables. The instantiator takes a block, the DSL input, and simply runs instance_exec on the block so it's executed in instance scope. Hand the DSL object with all the ivars to something else to actually do things with. Nothing in DSL behavior should do anything other than set ivars. The only metaprogramming is instance_exec.
Apologies if you took my posts as trying to argue you out of a position. I think the developer happiness is a fairly important point. And aesthetics being what they are, different strokes for different folks. So, I was just chiming in that I have an amusingly contrasted experience.
To that end, thanks for keeping the discussion going! This is a social site, so it is fun to flat out be social. :)
Oh I certainly don't see you doing that, that was why I said "I don't think we have any points of contention left to argue over." It's rather unfortunate that in a textual medium, tone must be inferred from prose. I just wanted to add a bit of extra color.