Data notation in Clojure
ostash.dev
ostash.dev
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 :)
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.
https://en.wikipedia.org/wiki/Lisp_reader
"Unlike most programming languages, Lisp supports parse-time execution of programs, called "read macros" or "reader macros". These are used to extend the syntax either in universal or program-specific ways."
read-string is a convenience over calling read itself as it will take a string rather than needing you to create a java.io.PushbackReader yourself.
As such read and read-string were implemented as core to Clojure's self interpretation and implementation, not as part of a general safe serialization API.
Unfortunately the temptation was to reach for read-string for de-serialization in general as it was so convenient and in the core. In a dev setting where you control and trust all input that is fine. In other contexts it definitely is not!
(let [a #d (+ b c)] ...)
So the #d reader macro has access to the following form, but gets ignored at a higher level (i.e. when the "let" form is processed)
(def #=(symbol "foo") :bar)
Gets read as (def foo :bar) (read-string "#=(launch-the-missiles)") ;; don't let randos launch missiles
(binding [*read-eval* false]
(read-eval "#=(launch-missiles!)"))Say I have `{:a 1}`, which is equivalent to `(hash-map :a 1)`, I take it as data and I can recreate the expression with the later form:
user=> (quote (hash-map :a 1))
(hash-map :a 1)
user=> (def a (quote (hash-map :a 1)))
#'user/a
user=> (list (nth a 0) (nth a 1) (nth a 2))
(hash-map :a 1)
but for `{:a 1}`, I got error since it's a bit different: user=> (def b (quote {:a 1}))
#'user/b
user=> (list (nth b 0) (nth b 1) (nth b 2))
Execution error (UnsupportedOperationException) at user/eval12 (REPL:1).
nth not supported on this type: PersistentArrayMap
user=> b
{:a 1}
now it's different, it's no longer homoiconic in this syntax. user=> (first b)
[:a 1]
I would rather not using this syntax in my own lisp and still using a prefixed version.