Good morning. A small think piece. Would Clojure be more popular if it was written in M-expressions? https://lucidglitch.com/posts/clojure-with-m/
have you seen https://docs.racket-lang.org/rhombus/index.html ? the Racket folks took a lisp and made it look like Haskell, it's pretty neat
@raspasov I'm not sure what should be retained exactly. I know FP langs normally don't have early return, break and continue. At the same time, the lib is tentatively called "familiar" π. And it's more of an exploration in can we make Clojure more familiar but still retain it's essence. Unlike my procedural lib, which, just adds imperative programming to Clojure (that one I use for leetcode like exercises π).
I feel like return, break and continue could make sense, I don't find they break out too much from Clojure's immutability and functional. But they really help to "flatten" out things, and sometimes can make things more readable or a bit easier, then having nested branching. So I'm not sure.
I am definitely not the target audience at this point, haha.
Not M-, but YeS-Expressions, if I got that correctly, but definitely a form of "Clojure without the parens", that one can use today: https://yamlscript.org/blog/2025-07-18/fun-fridays--dragon-curve/
@dankelieberkorb I looked at it, and was like, oh ya this looks really good (then I realized I was looking at the Clojure example lol). The parenthesis have disappeared from my brain lol.
yamlscript is pretty fascinating
Yes. Racket is powerful
Technically, couldn't we write ide plugins that rework the text however we want? You could transform Clojure into M or C-style syntax.
The trick is ensuring the line numbers match.
But the syntax, or lack thereof, is powerful. Would newbies feel better if the first parens, on each line was stripped of its outermost. If the first parens feels like a stop sign. Could we rely on indentation here. Translating to C like syntax sounds like loosing a lot of ability. My question is more: Is it that opening parens that is the stop signal for people? Or is it the tree of code, rather than blocks?
The βtree of codeβ exists in all languages. The only difference is that S-expressions donβt hide it. S-exprs also make it βeasyβ to make the tree deep in one line.
I didnβt use the very first versions of Clojure, but to the best of my knowledge, it did benefit from reusing at least some existing S-expression tooling at the time. Are M-expressions supported by any existing editor tooling?
Yes. Emacs knows s-expressons. And we could use a lot of slime early on. A few hacks turned up early. I don't think M expressions ever existed in software, just on paper. But who knows
i do think that any conversation here should have a discussion of how syntax changes would affect macros
the Rhombus folks put a lot of effort into making it work with macros, they invented a new intermediate syntax representation called "shrubberies" which are what get passed into a macro (over the surface syntax)
Whenever I see posts like this it always reminds me of https://github.com/videlalvaro/clochure?tab=readme-ov-file#clochure---a-better-clojure
Rust tried to have complicated C-like syntax and also have hygenic macros and it means that people either use raw "token trees" or a very limited list of syntax trees https://lukaswirth.dev/tlborm/decl-macros/macros-methodical.html#metavariables
And my intent with the post is not to propose some change at all. S-expressions is the best thing since sliced bread. But why do so many find it so difficult? I have heard that people thinks that it is the opening parenthesis before the first word that feels like a stop sign. I can relate to that, rather than saying there are too many of them. Saying that it is like a stop sign is something to understand. It makes some sense. Sure, there are many, when they stack up in the end on a single line, and we don't care about that. But perhaps that's scary. I feel very comfortable as those parenthesis define the structure, and so do you. I don't find it difficult to program ML languages either. These languages are clean. Imperative is nerve-wrecking for me. Finding every assignment, and keeping that in my head. How can all those programmers either be so smart or plain ignorant. I was ignorant until I saw my first few lines of Erlang in early 90th. I didn't know better. I'm ranting, but you know what I mean. I have seen a big Clojure code base be swapped out for typescript, with the motivation that Clojure is too difficult to understand. It's hilarious and sad at the same time. How is this possible? Sure, there was something else behind it, but how can such statements get traction?
I have no data to back up this guess, but personally I would guess that some of the difficulties of understanding are less about the syntax, and more about figuring out how to write code with very few or no mutations.
I think you are very right. Those familiar with functional programming doesn't find it difficult. Some of them just don't like the syntax. It's just easier to say the syntax is too weird, when you cant grasp working without mutation. I taught a team of java programmers Haskell, prior to introducing Clojure, just because they said static typing was so important. My thought was that Clojure would be a relief, after trying Haskell. Some stayed with ML languages. They didn't find it difficult.
Personally, I'm very happy with Clojure as it is, and I don't feel it needs to adapt itself to the preferences of mainstream or larger audiences. But ... if the lisp syntax truly is a barrier to entry (Andy's comment might be more on-the-money, I'm not sure), I thought a view layer that maps perfectly to Clojure itself might ease some new folks into using the language. And after a few months of that, maybe that view goes away entirely and they feel comfortable using lisp proper.
It has the benefit that it lives at the level of developer preference, and allows them to work alongside Clojure devs who are comfortable with the language as is.
I think it's not so much the parenthesis as the common constructs that are the issue.
If you try writing Clojure with my lib here: https://github.com/xadecimal/procedural
I'm sure people would find that way more familiar, and it eliminated that rightward drift.
The first culprit is let. Other languages have function scoped variables, but not Lisp. That immediately creates so much more nesting.
And then looping, your standard for-loop is simple and flat. Not so in Lisp.
One of my favorite features is recursive style looping that's as efficient and safe as iteration.
Not saying we need procedural programming haha. But I think it's not the parentheses. Common Lisp is imperative and procedural. But still it doesn't have function scope variables. Instead of:
(defn foo [a b]
(var sum (+ a b))
(inc sum))
You do:
(defn foo [a b]
(let [sum (+ a b)]
(inc sum)))
And it has a pretty weird and confusing block level "return-from".
Imagine doing:
(defn foo [a b]
(when (nil? a) (return 0))
(+ a b))
Instead of:
(defn foo [a b]
(if (nil? a)
0
(+ a b)))
In more complex examples the nesting gets even bigger.
I think in theory you could have function scoped locals, early return, and other more familiar constructs that helped flatten things and make things feel less like a parse tree. While still being immutable and functional. Though even in languages like Java, functional programming starts to introduce a lot more nesting.My opinion is that, attempts at a non s-expression Lisp have failed, but what might succeed is an s-expression Lisp with more classic "flat" constructs.
Part of me feels like trying to accommodate this "too many parenthesis" issue is admitting to a problem that's not really a problem. I mean, I know it's true that there are those who just simply look at the code and, without really trying to basically learn the language, immediately reject Lisp/Clojure because of this. But maybe accommodation isn't the best strategy.
so it depends what clojure's goals are, right. if the goal is to make the language as nice to use / practical / coherent as possible, then yeah, it's very hard to do that while admitting other goals like "easy" (close at hand, familiar). but if the goal is mass adoption, then it makes more sense to try and make the syntax more familiar to programmers from an imperative background
this is something i've watched happen in the rust community, the original language was quite focused on memory safety and concurrency safety and they've been trying to make it fit wider and wider use cases over time and it makes the language much more complicated https://jyn.dev/the-core-of-rust/
Hence why I suggest that, if this direction is explored, it should be done in a way that does not impact clojure lang/core.
There's something here that keeps reminding me of Lisp Flavored Erlang (although that's an attempt to make a language easier by looking CLOSER to good old comfortable, familiar, paren-laden Lisp). The idea/goal of offering an alternative syntax to make things easier and more familiar is hard to separate (in my opinion) from defining a new language. Much like LFE - it is 100% the Erlang platform, but it's really a different language, no?
this is also clojure's relation to Java IMO
which makes me think that the "alternate language" people are describing looks a lot like scala
So, what relationship does this overall topic have to Clojure then? Why not simply consider a new "m-expression JVM language"? (Not addressing this question specifically to you @jyn514, just the overall discussion)
extremely funny to me that the scala thing got such a strong reaction, i didn't realize there was a rivalry lol
Technically, in theory, any language be transpiled to another. In practice, thatβs βeasierβ between some langs, and borderline impossible between others. (arenβt there some projects that transpire Java to C# and vice-versa?) Iβm guessing this M-expression idea, if a fully formed, can be a transpilation on the βeasierβ side. Macros are a big question mark, as discussed.
Dunno if it's a rivalry, I think Scala ended up being more popular (fwiw). It's just a yucky language in my opinion.
I donβt think thereβs rivalry haha, more likeβ¦ Itβs such a different world. Almost 180 degrees from Clojure.
i am not convinced that M expressions are much more familiar from an imperative background FWIW
i think starting from there as an assumption loses a lot and gains little
Yeah, on the surface, it looks like S-expressions with mostly [] and no leading open bracket. Still looks weird.
> "alternate language" Not what I'm talking about. I'm talking about just swapping out the implementation of the clojure reader.
It'd be interesting to know the data on developers who really learned Clojure, did some amount of work on it "in anger", and later left/abandoned the language, I wonder if "too many parentheses" would even be in the top 10 reasons for leaving Clojure. I suspect not, which I assert without evidence π
Macros will work fine as long as the AST is the same. Macros don't operate on data, they operate on AST.
To further explain the reaction: I think most people in the Clojure community have a well-reasoned skepticism of static typing for many use-cases. And Scala is perhaps the most βstaticβ language on the JVM one can get. So thereβs that. FWIF, Iβve heard the latest Scala (Dotty?) has cleaned up a lot of things but I have zero experience with it.
It just so happens that, on the lisp side, AST is also data
Wanted to add that qualification βοΈ before I was called out
For Rust, for example, as far as I understand, at least the static typing buys you something: ability to do abstraction and memory safety in what is a very-low level language, without overhead (correct me if Iβm wrong). On the JVM, everything is already memory safe via the JVM itself.
Would swapping out the reader make it difficult to mix "M-Clojure" with standard Clojure? Would there be any kind of interop? What might the mix n match boundaries be?
βI wonder if "too many parentheses" would even be in the top 10 reasons for leaving Clojureβ Almost certainly not. I guess the whole premise of this discussion is βpeople who donβt get to that point in the first placeβ.
The result of reading M-Clojure or C-Clojure should be the same as regular Clojure: data structures that the compiler can handle. The only difference is the text.
And the mapping needs to be bi-directional. So you need to go from data -> text, and text -> data.
Now, how do you do that while keeping line numbers synchronized? That's tricky.
As an βn of 1β: I do remember when first started learning Clojure. () was almost definitely not the biggest hurdle to jump over. βGrokingβ no mutation was. For a week or two. I think something like Go, apart from the enormous corporate backing, was familiar in both syntax and overall semantics, i.e. pervasive mutation is welcome.
Another thing folks aren't used to is nil punning.
The idea that nil means "nothing", not null
Yesβ¦ but I think thatβs almost a detail, compared to no mutation.
"almost certainly not" - I suppose that's the frustrating thing then, that the anti-parens sentiment should be dismissed as not a serious concern to anyone considering the language, since we have (cough, hypothetical/anecdotal) data showing that the parens are not really a big deal.
As an alternative to all this, I've thought about writing a friendlier Java wrapper around Clojure. Where the sequence functions are actually class/interface members and you can use it like you can a Java 8 Stream instance.
Fortunately I haven't needed to return to Java yet (fingers crossed it stays that way).
Well, we donβt know how many people outright dismissed the language because () looks weird. My total zero data guess is that the answer is βmanyβ.
Yeah I think this whole conversation is about trying to bring in the folks who took one look at the syntax and dismissed it immediately.
Elixir in a lot of ways is Clojure with "ruby" syntax. And... well... maybe it is actually becoming more popular than Clojure as time goes on π But I don't know if it's so much due to the syntax being Ruby, or more that they went full-in on targetting the Ruby/Rails crowd, with their one single framework everyone use, superb documentation/noob friendly material, clear this is how you do things (a lot brought over from Erlang OTP, which is an existing convention), and maybe the appeal of the BEAM.
At the same time, this is not a valid reason to change anything. And as discussed, () have many things going for them. Perhaps critically, afaict, no one knows how to do macros in a simple way without them.
That's funny because I know a couple of companies that are migrating off Elixir and onto Clojure π
Oh wow
I would have thought that someone choosing Elixir knows why they chose itβ¦
Messaging apps, etc. I think thatβs where Erlang, Elixir really shines due to OTP.
I wish more dev leadership took the time to perform due diligence. Sadly that is too often not the case.
Elixir for a βstandardβ web app that will send/receive queries to one or a few databases? Questionable utility.
As a offshoot of my procedural lib, I have something baking that would just mimic the familiar syntax constructs, but remain fully immutable and not be imperative. Here's a stupid example just to show what it can look like:
(defn analyze-nums [xs threshold]
(let n (count xs))
(if (empty? xs)
(println :empty)
(return {:status :empty})
(else-if (<= threshold 0)
(println :bad-threshold)
(return {:status :bad-threshold :threshold threshold}))
(else
(println :starting n :items)))
(for item xs
(if (nil? item)
(println :nil-item)
(return {:status :inletid :reason :nil}))
(else-if (not (integer? item))
(println :not-integer item)
(return {:status :inletid :reason :non-integer :item item}))
(else
(println :letid item)))
(for (let i 0) (< i n) (inc i)
(if (> (nth xs i) threshold)
(return {:status :too-big :index i :letue (nth xs i)})))
(for [(let i 0) (let sum 0) (let zeros [])] (< i n)
[(inc i) (+ sum (nth xs i)) (if (zero? (nth xs i)) (conj zeros i) (else zeros))]
(if (= i (dec n))
(let cur (nth xs i))
(return {:status :ok
:sum (+ sum cur)
:zeros (if (zero? cur) (conj zeros i) (else zeros))}))))
Versus JavaScript:
function analyzeNums(xs, threshold) {
const n = xs.length;
if (n === 0) {
console.log(":empty");
return {
status: "empty"
};
} else if (threshold <= 0) {
console.log(":bad-threshold");
return {
status: "bad-threshold",
threshold
};
} else {
console.log(":starting", n, ":items");
}
for (const item of xs) {
if (item === null || item === undefined) {
console.log(":nil-item");
return {
status: "invalid",
reason: "nil"
};
} else if (typeof item !== "number" || !Number.isInteger(item)) {
console.log(":not-integer", item);
return {
status: "invalid",
reason: "non-integer",
item
};
} else {
console.log(":valid", item);
}
}
for (let i = 0; i < n; i++) {
if (xs[i] > threshold) {
return {
status: "too-big",
index: i,
value: xs[i]
};
}
}
for (let i = 0, sum = 0, zeros = []; i < n;
[i, sum, zeros] = [i + 1, sum + xs[i], xs[i] === 0 ? [...zeros, i] : zeros]) {
if (i === n - 1) {
return {
status: "ok",
sum: sum + xs[i],
zeros: xs[i] === 0 ? [...zeros, i] : zeros
};
}
}
}I think thatβs one of the big imperative things that people are βused toβ. return anywhere, anytime.
Probably not a good idea.
lol the βteacherβ agrees
Is it a good practice to leave a repl in your production backend server? If so, how do you handle this with a load balancer adding or removing containers?
If you're in a container environment, you don't even need SSH, you can use docker/kube exec instead. That way you don't even need an SSH server in your container.
localhost only repl, can be accessed via ssh. you'll most likely want to use port forwarding.
exposing a repl directly to other hosts is a bad idea, and you'll likely need to manage ssh access for your load balanced / container managed instances already
At my last job we had a chat system built on http://socket.io, and I ended up writing a thing to connect so you could connect to a clojure.main/repl over it (had a server and client component). Had a simple public key based with a system. I think I was the only one to ever use it.
The plus with that approach is it piggie backs on whatever you are already doing to interact with the web API of your app, no new ports, lb, container stuff whatever
https://gist.github.com/hiredman/86aeb916b478d9e57cbce8e0e678babd dictated but not read similar sort of thing using just http.
The downside of that kind of thing is in large orgs you likely want something more auditable which you can kind of get for free with ssh and port forwards
Has anyone tried repurposing the filename and line number attributes provided by the JVM's https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/StackTraceElement.html#getFileName() to improve source mapping stack traces from exceptions?
Ah nice.
IMHO basing Clojure dev tools on file/line/columns will always be lacking, since the Clojure compiler (as all Lisps) are not file based, but form based. You can eval things on the repl and they will not have anything files, lines or columns related. I know it can be seen as "file based" from a practical perspective, because most systems runtimes are built via file loading, but doesn't have to be the case. That is why FlowStorm, and Cider debugger goes with forms + coordinates instead of file,line,column. When I was starting with FlowStorm I remember researching the JVM debugging capabilities to try to use that also, but it is all line based, and that is baked on the class format. I remember also trying to encode line+column together on the line field, but there wasn't enough room. I think in that respect all the JVM is designed for statement based languages where line is the thing. I know IntelliJ java debugger can jump to different expressions on the same line, but not sure how they are doing that, probably a hacky thing.
Also in non interactive languages, the files are much more in sync with the runtime, because you are always compiling everything and the running. But in Clojure you could have a runtime with a very different state than you files if you have been moving things around and just [re]evaluating some of them
> But in Clojure you could have a runtime with a very different state than you files if you have been moving things around and just [re]evaluating some of them And since I'm going to be reading directly from the files on the classpath, this is definitely going to come into play. I have some ideas for addressing it but that'll come later.
But that's a really good callout.
Well @jpmonettas can't FlowStorm retrieve the form that caused an exception? Would that work for @smith.adriane?
I think @smith.adriane is aware already of FlowStorm's capabilities in that respect since he was experimenting with it, but I think he is also exploring different ideas around exceptions and relating them to the source code
@jpmonettas explains the problem well.
It seems plausible that you can use Java's StackTraceElement's filename and line number to identity specific clojure forms:
> (try
(push-thread-bindings
{Compiler/SOURCE (str (random-uuid))})
(try
(eval (with-meta
'(throw (Exception. "hi"))
{:line 42}))
(catch Exception e
(let [stacktrace-element (-> e
.getStackTrace
first)]
[(.getFileName stacktrace-element)
(.getLineNumber stacktrace-element)])))
(finally
(pop-thread-bindings)))
;; ["d824f8ce-591c-4b9d-b433-45d1cd7efabe" 42]The idea being that you can use the filename to identify a top level form (eg. foo.bar/baz) and the line number to specify specific subforms.
yeah, that is interesting, assuming you always control the eval (like in the case of easel/clobber) but systems can start many repls you possibly don't control that can push code into the runtime
Oh I see. Jp was describing the original problem that phron is trying to tackle. Gotcha π Yes that makes a lot of sense.
Although what do you mean by form + coordinates? Aren't the coordinates just line/column as if the form was itself a standalone file?
they're related problems. when you get an exception, you get a stack trace. A stack trace is a list of stack trace elements. A stack trace element only has a handful of fields (eg. classname, filename, line number, and some other mostly useless stuff). If you're instrumenting like flowstorm, you can capture more info. If you're not instrumenting, then you only have whats in the stacktrace element. Even if you are instrumenting with flowstorm, it currently only has information from what the reader produces. Clojure's reader only does file, line, and col. However, line numbers are relative and can change after evaluation. Additionally, the reader throws away a lot of source information (eg. comments) and doesn't provide line and column info for all forms (just lists?).
Although what do you mean by form + coordinates?FlowStorm doesn't use [file,line,column] as identifiers for expressions but [form,coordinate] where coordinate is the position in the sexpression tree, like a get-in coordinate.
So in (defn sum [a b] (+ a b)) form the (+ a b) has coordinate [3], while the a in that expression has [3 1], etc. This system is not dependent on how you format/display that form, and has nothing to do with files, although file,line,column still is being stored when available but they are kind of optional "second class"
Ah right my apologies, I had bookmarked the discussion and confused them somehow
The main problems I'm looking at are: β’ line numbers getting out of sync during repl development β’ no column numbers β’ explicitly mapping to clojure expressions rather than just line numbers. β’ mapping generated code that doesn't use a text file as its source
Using cider nrepl, I've been pretty content with exceptions so far. Haven't done anything like the above when it comes to exceptions, specifically. But I'm working on a dev tool that will execute your code form by form and give you all the intermediate outputs. You'd just have to rerun your code if you hit an exception. Would that fit your needs?
I could probably give you all the metadata you're looking for since edamame is my form parser.
I am also working dev tooling and am looking at different options.
I'm basically interpreting the Clojure source, macroexpanding, and calling apply on every function.
That's something I can do, but I don't think it addresses any of the problems I listed.
Actually I think it addresses most. It's just not the simplest solution. Since edamame provides line and column info as metadata on the form, and since I'm pulling from the files on the class path directly.
The generated code one is something I'd need more info about. Not sure about that.
It'll mostly work, but if it uses any let bindings outside of interpretation, those symbols won't be found.
If you define function foo at line 23, evaluate it, then add a new function bar above foo, then foo will no longer be at line 23.
there would still be no column numbers
If there are multiple clojure expressions on a single line, then the exception stack trace element still won't differentiate between them
Maybe I'm not articulating myself well. You are correct assuming default Clojure evaluation. I've been talking about the specifics of the tool I'm working on.
If what you are looking for regarding your above bullet points is just better info for a better time debugging, I think I can help with that.
If your requirements are different, then I'm not sure.
Do you have a link?
Yup. Alpha is not cut yet. I think I'm a week or two away. But if you'd like to see what I'm going for, here it is. http://github.com/lambeaux/paper-trail
I might be interested. I've also been playing with flow-storm. Do you have an overview of how it works?
The data I'll be able to provide to you on failure will be very rich compared to what a plain exception gets you.
For context, this for https://github.com/phronmophobic/easel/https://github.com/phronmophobic/clobber.
Sure. You invoke a FN like this
(my-fn x y)
You see output you didn't expect or an exception you didn't expect, so you rerun using paper trail
(pt/trace-fn my-fn x y)That's it.
I'll return a trace report as data
If my-fn calls my-fn2 and the exception happens in my-fn2, does it also give you info about my-fn2?
If you want it? Yes. I can trace nesting for as deep as memory is available in your JVM
Still trying to figure out good defaults for that
Do you know how papertrail compares to flow storm?
Yeah. Flow Storm has more features and more power when it comes to recordings. There are a lot more richer visualizations you can make with it. But to achieve what it's doing, it's doing some code instrumentation that generally makes it unsafe for production.
Now, you could argue interpretation is a form of instrumentation, but at the end of the day, paper trail is still just calling apply on uninstrumented code. The goal being to be production safe for live persistence of tracing.
How does papertrail gather info if papertrail isn't using instrumentation?
papertrail is an interpreter?
Also to be fully host agnostic
Technically? I'm not calling eval anywhere. I have my own call stack.
For me, having first class JVM support is very important.
Look in paper trail repo in the test files
The tests are just forms. I call Clojure's eval and my eval, normalize away some exception nuances, and make sure they match up.
They should be at parity 100% otherwise what's the point?
Anyway, if you want richer exception information, I'm just throwing this out as an option.
Something simpler might make more sense for you.
But interop will absolutely be supported.
It's a non-negotiable in my mind.
Yea, it doesn't quite seem like what I'm looking for, but I'll think about it. Thanks for the input!
When I have a decent demo showing the info I can extract on an exception I'll be sure to let you know.
@smith.adriane I think there might be some relevant points to your question in this thread https://clojurians.slack.com/archives/C015LCR9MHD/p1759850333063289 It is in the context of SCI and not JVM Clojure, but the solution in both has some overlap I believe
@jeroenvandijk thanks for the link. Can you summarize the solution and how it could be applied on the JVM?
@smith.adriane It's both about providing better error messages. On the JVM you might need to work harder to get the right column info whereas with Sci you get this for free, but in both cases error messages can be improved by identifying typical cases such as (+ 1 nil) giving a null pointer where it could be more specific of expecting a certain type. I think the thread is useful for insights and finding a common ground. Maybe it is not an exact solution to the problem you are describing
This thread was mainly asking about a specific technique that might help on the JVM. I'm mainly focused on the JVM target right now. I also had a previous thread on error messages more generally, https://clojurians.slack.com/archives/C03S1KBA2/p1756162447266729.