Hey all,
After the whole Racket2 chat, which if you are not aware, includes a proposal to get rid of s-expressions in favor of a more familiar syntax to try and attract non Lisp devs to it, I started to toy with the idea of why is it people struggle with the Lisp syntax, and could a simple macro help make it more familiar.
With my first pass, I ended with a macro I called fam
which allowed one to write code as such:
(fam
var data = [{:id 1 :name "John"} {:id 1 :name "joHn"}]
var group-by-fn = :id
(group-by group-by-fn data))
;=> {1 [{:id 1, :name "John"} {:id 1, :name "joHn"}]}
(fam
var x = 10
var y = 30
(+ x y))
;=> 40
My thought was that the biggest challenge devs face is the fact Lisp code is normally written inside out, which tends to create a kind of rightward drift in the code, and lots of parenthesis. So I thought why is that? And it is mostly because of style, in other languages, people tend to flatten their code a lot more using intermediate variables, but in Clojure we tend to just nest arguments in arguments. Even if you use let
, that also forces a rightward nesting.
Now I starred at this for a while and I actually started to like it myself. But I realized initially I was going for familiarity, so I even chose a familiar syntax using var =
to define the inline variables. But when looking at it for me, someone familiar with Clojure, I didn’t really care for that, but I thought the flattening, or reduction in rightwards drift was even making things easier for me to read. And I realized that’s what most threading macros do already, and those are really idiomatic…
So on my second pass I ended up with this:
(-<>
<data> [{:id 1 :name "John"} {:id 1 :name "joHn"}]
<group-by-fn> :id
(group-by group-by-fn data))
;=> {1 [{:id 1, :name "John"} {:id 1, :name "joHn"}]}
(-<>
<x> 10
<y> 30
(+ x y))
;=> 40
Which is actually starting to feel way more idiomatic to me, and no longer so drastic as with my initial fam
macro.
Then I realized that sometimes names are redundant and when you just need to thread the result of the previous thing, they just get in the way, so I extended it to also support:
(-<> (range)
(map #(* % %) <>)
(filter even? <>)
(take 10 <>)
(reduce + <>))
;=> 1140
This works like an implicit as-> (range) <>
, where <>
always contains the value of the previous expressions. And now you can mix and match as well:
(-<> (range)
(map #(* % %) <>)
<evens> (filter even? <>)
(take 10 evens)
(reduce + <>))
;=> 1140
And if you’re annoyed about a thread whose result should always be last or always first, you can nest your choice of threading macros inside:
(-<>
(range)
(map #(* % %) <>)
<evens> (filter even? <>)
(->> evens
(take 10)
(reduce +)))
;=> 1140
(-<>
<my-string> "a b c d"
(-> my-string
.toUpperCase
(.replace "A" "X")
(.split " ")
first))
;=> "X"
And finally, I thought this can all come together as just being the default Clojure syntax, so I extended defn and fn with implicit -<>:
(<defn> lexo [x]
<rng> (range 1 (inc x))
<cmp> #(compare (str %1) (str %2))
(sort cmp rng))
(lexo 13)
;=> (1 10 11 12 13 2 3 4 5 6 7 8 9)
((<fn> [x]
<rng> (range 1 (inc x))
<cmp> #(compare (str %1) (str %2))
(sort cmp rng))
13)
;=> (1 10 11 12 13 2 3 4 5 6 7 8 9)
And if you want to use #(), it’s easy:
(#(-<>
<rng> (range 1 (inc %))
<cmp> (fn[a b] (compare (str a) (str b)))
(sort cmp rng))
13)
;=> (1 10 11 12 13 2 3 4 5 6 7 8 9)
I plan to open source this, but I’m looking for feedback beforehand. What do you all think? Is this a good addition to Clojure’s syntax? Does it actually make things easier to read and parse even for experience Lisp coders? And what about non-lispers? Do you think that is still too cryptic for them? Any ideas to improve on the syntax even more? Ideas of other things it should support?
Thank All!