I've yet to see a better approach than Ruby's.
I've yet to see a better approach than Ruby's.
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.
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.
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.
The format function can take a function as an argument in place of the format string.
The formatter macro can compile a format string into such a function.
(format nil (formatter "(~a-~a)") x y)
formatter spits out a lambda that is passed to format; format then passes it the stream, and the remaining arguments.Why don't you consider macros like DEFUN, DEFCLASS, COND, etc to be "syntax breaking"?
defun could be like this, though its ANSI CL specification is quite screwed up.
The sytax is given as:
defun function-name lambda-list [[declaration* | documentation]] form*
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.
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.
(loop :for i :below 10 :do (print i))
Is less "syntax-breaking" than: (dotimes (i 10) (print i)) 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.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))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 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.
To that end, thanks for keeping the discussion going! This is a social site, so it is fun to flat out be social. :)
I find that the language is flexible enough that I never really reach for meta programming when tackling problems.