swoon!
For a recent example of why you wish your language had macros, look at function builders in Swift. When SwiftUI was first announced its syntax was a huge wtf, only later did we find out that a new language feature was enabling this syntax.
Now imagine if somebody outside of Apple had wanted to do this and had to convince the Swift dev team to incorporate this feature...good luck.
In any given lisp, function builders are a 10 minute project to make a macro.
Part of the very real value in being able to print the data exactly as you'd type it in as a literal is repl-driven development and production logging. I can effortlessly print something to the production logs, copy/paste it into my development environment, and debug. So while it's true that string (de)serialization is an old idea, very few languages support one-to-one copy/paste (de)serialization. So often, other languages give you <Object_0xffa003> or something if you try to call x.toString(). Clojure makes it easy to move data around between systems, by emphasizing extensible, yet simple data structures.
(let [cases [{:in [1 2 3] :out [2 4 6]}
;; ...
]]
(doseq [{:keys [in out]} cases]
(is (= out (test-fn in)))))
If I want to inline the test cases, I can rewrite it like this (let [cases [{:in [1 2 3] :out [2 4 6]}
;; ...
]]
(map (fn [{:keys [in out]}]
`(is (= ~out (test-fn ~in))))
cases))
Then eval that in the repl and copy the result back into the test file. It’s a bit clunky because the repl output uses fully qualified symbols, but cleaning that up is usually faster that manually typing out the code it generates. (macroexpand '(defn foo [x] (println x)))
Returns: (def foo (clojure.core/fn ([x] (println x))))
Clojure itself is built from macros. Even something simple like defining a named function with defn.As for macros, speaking from experience, I think custom macros are rare because there are only three real use cases: First, was to reduce duplicate code. Second, was to extend the language if it was missing a feature. Third was to prevent expansion of some form (ie logging out a infinitely expanding form). It's a hard syntax to grasp sometimes with all the `~@form type stuff. I used to chastise developers on our team for writing macros because they would often write macros that really did none of those (like a custom thread-> macro). They are pretty _fun_ to write though! :)
Edit: I wanted to add some more thoughts to this. I also think the lispyness of Clojure is overplayed to the end developer, but a large amount of the Clojure core language forms are macros (and, or, ->, for, etc). I'm speculating here, but I'm guessing it was how the Clojure core language became relatively less verbose than other lisp dialects.
https://download.clojure.org/papers/clojure-hopl-iv-final.pd...
Rich Hickey goes into detail how he designed and implemented Clojure. One thing that stood out was a focus on a really small, conservative core, where the vast majority of clojure’s features are implemented as libraries, based on a small, core language. Other languages would need to extend the syntax to achieve the same.
A great read, and I don’t program Clojure in any respect, but I do want to after reading it.
If there is a small core, it's not necessarily obvious. Yes, a lot of things are pushed out to libraries, but even the basic language features, liked (defn) (cond) (ns) threading etc. - are all implemented through macros. There is prolly a small subset of the language is which is actually made of language primitives implemented in Java or JS. From a language development point of view this probably makes life much easier. And if you wanted to port Clojure to a different backend then that'd probably be much easier than a language where syntax is added more explicitly. There is a design elegance to it, but as a language user it really makes no difference if a feature is a macro or language primitive. I mean to say Clojure isn't big, but it's not some barebones Scheme-y thing either. It probably has a Scheme-y core, but you'd need to root around to find it
A macro system that manipulates the syntax tree directly has little to do with homoiconicity.
The real advantage is simply that the syntax is very simple and consistent but there is no need for it to be homoiconic to be that.
Clojure's lispyness is a bit overplayed? I feel it's generally irrelevant for the vast majority of language users and it's really not what makes the language great. I seldom seeing custom macros... and while I'm sure it's important for a minority and I'm glad it's there - if it was disabled tomorrow (outside of the language internals) I'd probably take me a while to even notice :)