Why Clojure?(gaiwan.co) |
Why Clojure?(gaiwan.co) |
The biggest problem I had (and still have) with Clojure is that, even in its heyday (circa 2014-2018 or so) I couldn't convince people to keep using it. It seemed like every smart, senior engineer I knew tried it, loved it, wrote load-bearing stuff in it, and then walked away: either abandoned it at their companies, abandoned it for personal projects, or started companies using other languages instead of Clojure. And this was when Clojure was hot shit. Big Data was all the rage, and everyone had heard of or was using Storm. Clojure was the language.
You might counter that maybe me or the people I know are fools, not smart enough for Clojure, or picked stupid languages for the wrong reasons, or followed the crowd, or whatever else. And you might be right!
But all I know is that the smartest people I know tried Clojure, learned it, respected it, learned a ton from Rich about how to think about systems, and ultimately walked away, because they were, variously, repelled by what they perceived as:
- the elitism in the community
- the elitism in the core team
- the impenetrability of the tooling and the unwillingness of anyone to admit that it sucked
- the sometimes not so subtle denigration of their skills as professionals for having the audacity to, for example, use web frameworks rather than build everything from scratch
I bring this up because for me programming is social. I love being able to write code that does something fun and/or useful and tickles that part of my brain that loves expressing what I want in a beautiful way, but ultimately it's more useful to be to be able to write code that my friends and colleagues can understand and are willing to use.So I have to wonder, is it sustainable for a community to be as reliant on true believers as Clojure seems to be? Is it sustainable to alienate as many of those folks who maybe aren't true believers, but who would love using it anyway? Who knows, maybe it is! But I can tell you: I'm Clojure's audience, and I don't use Clojure today, and none of my now-Staff and Principal and CTO level friends are using it or teaching it to the juniors on their teams or choosing it for their companies. Why? Wouldn't it be better if we were?
> "But what about hiring?" When you use any language that isn't in the top 3 of currently most popular languages, you will get this question. JavaScript programmers are counted in the millions, Clojure programmers in the tens of thousands. How will you ever find the required talent?
is it just me or i, for the love of whatever that is holy, find that fp related jobs are so few and far in between? not only clojure, but also f#, haskell, etc? to be honest, fam, if you want to sell a language or two, at least make it possible for people to make a living in it. sorry to say, mate, but my kids can't eat a "lovely" niche programming language.
p.s., i actually have some working exp. in haskell yet nobody approached me because of it.
Clojure sort of guides you to simplicity, building everything out of functions and simple datastructures has big advantages when testing and reasoning about code.
I do find that in larger code bases, Clojure lack of types causes friction (spec is just a bandaid, not a fix).
There are languages with immutability and types (like Haskell), but these don't have the get-shit-done factor I seek.
OK.
> Thanks for the segway.
A segway is a motorized transport; a segue is a transition to a different topic. So this could be considered a type error, and demonstrates why you need types, not just immutability.
Interestingly, most of the low level libraries in the Clojure ecosystem are programmed that way. If you open up the code of many popular Clojure libraries, you see defprotocol everywhere.
So really, Clojure does both: dynamic typing for application programming, mostly static typing for infrastructure library code. As they should be.
This tired argument of "Clojure bad for dynamic typing" just doesn't hold water. Keep repeating it is a sign of lack of critical thinking.
Coders often accept the status-quo of things - "So what that we can't easily upgrade our dependencies in our Node/Ruby/Python project - it's a known thing, we just have to deal with it..."; "Of course our builds take 90 minutes, that's normal for a big project"; "We need complex state management like Redux"; "Code must be recompiled after every change"; "Testing requires mocking everything", "Type safety prevents runtime bugs"; etc.
Yet, when it comes to actual work, not some cheap talking points but real, practical scenarios - there aren't many other tools that can beat Clojure for robustness, pragmatism, and speed of building things with it.
Most experienced Clojuristas are incredibly knowledgeable, skilled, and seasoned programmers, architects, and system designers who are passionate about their craft, using tools, techniques and patterns that enable them to perfect it while remaining pragmatic and grounded. But hey, don't blindly trust my word, go see for yourself - open ClojureTV YT channel, watch their presentations and draw your own conclusions.
Very few languages still in use in the modern era are still weakly-typed.
My personal take is that while a lot of type systems can be overblown, nil-punning is not a sufficient substitute for optional/maybe types.
Personally, I think Scala is a fantastic language, but it's had breaking changes, a functional only ecosystem, Java -> Scala interoperability is HARD, and the language is complex. Now there is Scala 3 and there are more breaking changes and the tooling landscape hasn't caught up.
How often you hear of a language online does not reflect how much it is actually used. Nor does it reflect on how good the language is.
I would not compare it to SQL: I do not use SQL because there is no distinction of in-band and out-of-band data (your data and your commands/queries travel in the same channel, which causes a world of pain). But there are other database approaches that work very well. It's not a problem at all to serialize your data and store native Clojure data structures, even better than datomic does.
In other words, I disagree that Datomic is somehow natural or superior: it is "a" database, excellent for certain applications, not necessarily the best choice for every application.
In many use cases, database is where application state resides, hence a mutable database is a better fit.
There are Datomic flavored database in the Clojure ecosystem that is specifically design to address this point, e.g. Datalevin.
Complex static type syntax ends up infecting your code, because the syntax becomes load-bearing. A change to one type has a tendency to ripple through the entire body of code. This means you have tight coupling as a consequence of the syntax, which is a bad trade-off. Clojure's strong dynamic abstract types allow you to focus on the function of the code instead of its structure. Trying things out in the REPL first helps you find many of the things that you might need to wait for a compiler to find.
There are studies that estimate that the kind of bugs shipped to production that strong static types help prevent account for only 2% of defects. The rest are things like off-by-one errors, design errors, and incorrect logic.
The other thing a static type system can help you with is reading code. But it's primarily an aid to people unfamiliar with the language, or put in more popular parlance a "skills issue." We know tooling can navigate the code, because we have tooling that does so (language servers, CIDER, Calva, Cursive, Conjure...). This means that static typing for the purpose of merely reading the code is a crutch. I'd argue it's also a crutch or training wheels for writing correct code.
You can't run with crutches. You can't corner well with training wheels. Clojure is meant to make the expert nimble and fast. The main thrust of its design is to make experienced devs more productive, like professional tools without novice guide-rails make experienced craftsman more productive.
Rich Hickey gave a talk where he mentioned this idea of guardrails on the highway. He told a story of how we all get in our car, then bump into guardrails all the way to our destination... right? Of course not, we learn to keep the car in our lane of travel, to signal others when we switch, and to blend with other traffic on the way. (Rich was talking about TDD, but it still applies to static type systems, IMHO.) Clojure is that power tool without all the novice presets. It's the automobile we keep away from the rails when we drive it.
Mutable state is arguably responsible for far more defects shipped than type errors. Mutability is only a thing because hardware constraints required reuse of memory locations instead of allowing for persistent data structures. Thankfully, for "situated" applications that start and operate for extended periods, those constraints aren't so tight.
If you have to choose between immutable data and static types choose immutable data. But why not both? We can have all our luxuries, right? Because you have to spend so much more time proving things to the compiler, you have so much more syntax to manage. More code and more syntax for the same problem is almost always worse. I'll take Clojure.
If you've got a reference, I'd be interested. (Not saying you're wrong...)
> The other thing a static type system can help you with is reading code. But it's primarily an aid to people unfamiliar with the language, or put in more popular parlance a "skills issue."
It's an aid to people who are unfamiliar with the program, who have no idea what the shape of "that thing" is. (Especially, what is the shape of the thing that this function gets called with? For all possible callers?)
I've been hired to start working on code bases that have existed for a decade or two. Static, stated types are a life saver. Sure, they may slow you down while you're creating the code. It's easier to not have to explain everything that's in your brain. But when you and your brain leave, and I'm left with a bunch of variables that I have no idea what type they are - what are the maximum set of types that they can be for all paths through all callers - then I really need static types to help me figure out what's going on.
[1]: A Large-scale Study of Programming Languages and Code Quality in GitHub https://dl.acm.org/doi/10.1145/3126905
[2]: On the Impact of Programming Languages on Code Quality: A Reproduction Study https://dl.acm.org/doi/fullHtml/10.1145/3340571
There is Typed Clojure https://typedclojure.org/ for people who want actual static typing, but the fact that it never caught on suggests this wasn't a real pain point for most people using Clojure.
As other people mentioned, immutability tend to be a more important feature than static types because it makes it easy to write code that's referentially transparent. You can structure your whole application as a series of small components that can be reasoned about in isolation.
If you write code in a defprotocol everywhere style, as many Clojure libraries do, your code won't compile if you got the types wrong. The same as Java. How's that not static typing? Which part of that is weaker?
So what exactly this "Clojure dynamic typing" nonsense is about, I fail to see.
Automatically inferred type system is not the same thing as static typing. Typed Clojure is the former. Typed Clojure did not catch on, but static typing style of Clojure did, as many Clojure libraries do exactly that: internally, defprotocal everywhere, externally, some Clojure functions to give the illusion of normal Clojure code. BTW, that's the style how Clojure itself is written in as well.
No-one has the time to learn all these languages, so for those of us not in the know, the most generous we can be is to take these praise articles at face value. And these articles are typically 'dynamic good', 'static bad'.
From the article:
they insist on a statically typed worldview leading to parochial, snowflake APIs that defy abstraction and higher level manipulation, or both.
So I guess the user base for this language is those developers who think dynamic is better than static, and go for (as-static-as-Java) Closure.I find using defprotocol in Clojure tends to be an antipattern because it just makes code harder to read by introducing indirection. The libraries using defprotocol use it to solve a specific problem of creating a contract for how the API looks.
"You Keep Using That Word, I Do Not Think It Means What You Think It Means"...
When you say "types," can you please give a concrete example of what languages you're talking about? Whenever someone talks about Clojure from type theory talking points, they make it sound like it's completely untyped or weakly typed. Clojure has its own type systems, and you can express things in them that are far more difficult to do in some other languages, even with static types. It is also a strongly typed language, and in practice, I feel far more confident about our Clojure code than anything written in TS or Java.
Sure, static type systems have their value, and some of them are really nice. In practice though, whenever I have to jump in to deal with Java, Typescript, or even Rust code - there's so much seemingly unnecessary fluff, and I don't feel the practical worth of dealing with types - it almost always feels taxing. Haskell/OCaml is a different story - I can buy that one. But realistically, getting to the point of writing practical software in Haskell is an incredibly bumpy road, and (relatively) few programmers successfully achieve good milestones on it.
But I can't expect most codebases to use spec or malli. The comment I replied to said that's what the REPL is for. A REPL, while helpful for a lot of things, will not tell you about an argument faster than a type annotation.
It is not an anti pattern, it is the way most low level libraries and clojure itself are written.
Clojure is a tool, not a cult. This core team worship is turning people away. The core team made plenty of mistakes, and got called out, rightfully.
Most libraries are absolutely not written in this way either. Please point me to a single library that's actually written in the style you describe. The use of protocols in actual popular libraries like Ring tends to be minimal.
The reality is that dynamic typing has never been a real problem in Clojure. I've worked with the language almost exclusively for over a decade now, and I maintain a number of popular libraries, like Selmer, with millions of users.
Clojure gives you the option to run the compiler at runtime, so that's what people normally do. However, you can also run the compiler at compile time. Right?
For people who want type checking, they can opt to write Clojure in this defprotocol everywhere style, and turn on AOT. Then they basically get the same thing as what Java gives them.
As to example of libraries that are defprotocols everywhere, you should look at any of the low level performance minded libraries in the Clojure ecosystem, they are either written in this defprotocols everywhere style, such as nippy, neanderthal, dtype-next, and so on, or mostly in Java, such as http-kit, fast-edn, etc. I noticed this phenomenon, because my own libraries, editscript and datalevin, are written in this way. I take comfort that my fellow performance minded library authors are doing the same. Finally, isn't Clojurescript entirely driven by protocols?
So really, there are two kind of Clojure programmers. One type writes application code or high level libraries, and they write normal Clojure code all the time. However, there are also those who write primarily low level library code, which are used by the first camp, and their code is full of defprotocol and deftypes. So defprotocol everywhere is not anti-pattern. It's anti-pattern only in the mind of the first camp of programmers, and that's a narrow minded way of looking at things. Even the first version of Clojure Programming book by the core team members, are written in a way that's full of defrecord. Remember?
This "everything is a map" orthodoxy is turning people away from Clojure. Just let people write the code that suits their own needs. We can use more people who are pragmatic instead of dogmatic in the Clojure world. If you trust Rich Hickey's judgment, then you should trust him put in the features Clojure has for good reasons. Macros and protocols are part of the Clojure language, and you should be using them when the use case calls for them. Stop the "anti-pattern" nonsense.
These libraries aren't using protocols for type safety though, they're using them as a performance optimization. That's certainly a perfectly fine reason to use protocols, and I agree that it's a completely legitimate use case. It's the whole right tool for the job thing. If you're writing something where performance is the top concern, then that's what protocols are for.
I very much agree with you that that there are people who focus on low level code, and those who focus on application level code. The style of coding will be different depending on the type of problem you're solving. You're right that I failed to qualify my original statement regarding protocols being an anti-pattern.
You're absolutely correct that we should take a pragmatic approach towards using language features. Hence why the context of whether protocols are the right tool to each for lies in the type of code you're writing. And of course, some people find it easier to have more structure to help with their reasoning. Although, I'd argue tools like Malli work better there.
To sum up, I'm not arguing against protocols being useful or that there's no place for them. We started this discussion talking about whether protocols provide equivalent guarantees to Java's type system. I disagree regarding that. However, I also don't think that this is a real problem. Otherwise, use of something like core.typed would've become prevalent by now.
In any case, it is possible to write Clojure in a way that is type checked at compile time. And it is an acceptable way to write Clojure. I just want to clear the air.
If you find protocols are a helpful tool to structure code that's great, keep using them. Nobody is telling you not to. However, try to accept that different people use the language in different ways. Try to follow your own advice and not to be dogmatic about it.
I've created some great apps, and great libraries (in both Clojure and Java).
I often describe Clojure as "the least worst programming language", which is an off-handed complement, but I think accurate. Things you don't like can generally be fixed (at least locally) using macros and libraries. The core is strong, a good basis for building things ... and as described all over this thread, stable.
As you master your tools, you gain a level of speed and precision in your work that I have not found elsewhere. The REPL-oriented workflow is a central value proposition in Clojure, and many features (and a few limitations) of the language exist to properly support it.
Working in Clojure feels like I'm working "with" my code, molding it like clay. My prior experiences in Java and Objective-C were so much slower, with long code-compile-test-debug cycles that are compressed down to instantaneous responses in the running REPL.
I've been working on a large modern Java application lately and have never really felt the need for a REPL workflow even after having been exposed to it in Clojure. I tend to structure my Java code so it can be easily unit-testable and then just run the suite of unit tests (several thousand) in a few seconds as needed.
i find core.spec did wonders for that problem.
also, i have a tendency to write a lot of in-code documentation, whichever language i use. so it probably helps too.
I wish Hickey had built gradual typing in to the language.
I still love Clojure anyway.
What macro do I use to make it not run on the JVM? :)
But it runs on other things—Microsoft's CLR, Dart's runtime, JavaScript's runtime, Erlang's runtime, and with Jank, on top of LLVM.
You can still get mutability and I do this on every project. But it's a very small percentage of the code, less that 1%, and also well-defined.
Something like FlowStorm [0] isn't really practical in anything but Clojure, and things like Clerk [1] are easy and very natural.
Immutable data pairs great with REPL.
Because data are immutable, you don't need to care about where that data come from and where it will go. Just focus on what you need to do with that data at the point you work.
Everything is localized due to immutability.
The REPL let me test my changes while inside the thing as it ran. No problems. Someone wrote a nice *recording* debugger too which helped immensely -- no more "oops, I'm past the interesting part and have to start over"
* in prod we usually give it a small number of large instances
I'm used to using TDD for fast feedback as I'm molding my code. Do you miss unit testing? Or, do you find that the REPL in no way obviates unit testing?
And, do you miss static typing?
BTW, when Clojurians talk about REPL, it's not about that separate window where you type and run the code as in other language such as python. They are talking about an invisible REPL running behind the scene, to which they send code within their editors, and the results show up in the editors too.
There's no need to "miss static typing" in Clojure. If I need static typing, I just write deprotocol and deftype in Clojure, as many Clojure libraries do.
One can develop with TDD in Clojure quite smoothly depending on choice of tooling; with CIDER in Emacs there are keyboard shortcuts to run tests for the current namespace or the entire project, so feedback can be very fast (if your tests are fast). I've also used (some time ago) test runners that stay running and re-test when a file is saved.
In fact, it can be nice to do one's explorations in the REPL and then reify one's discoveries as tests.
Regarding types: I will say that working on larger Clojure (and Python) projects with somewhat junior teams made me more curious about type systems. Clojure's immutable collections and the core abstractions they are built around are great, but it can take some skill and discipline to keep track of exactly what kind of data is flowing through any particular part of your program. But, there is some support for à la carte strictness in the language via Spec, Malli, structured types, etc.
With clojure-lsp, deps.edn, and more REPL tooling (conjure in neovim in my case), the situation is better now. I find myself reaching for Clojure for almost everything these days - from scripting to data crunching to quick web apps to database work. Clojure is an amazing tool once you grok it - closest thing to a super-power we can get.
> working "with" my code, molding it like clay
This the best description. Clojure feels very fun/interactive but simultaneously feels rock solid for production work. There is no gap between "notebook" and "prod". Zero compromises. Most other languages pick one or the other (Python - interactive but plagued by runtime errors, Rust - rock solid but clunky to iterate and experiment)
If REPL is the main value proposition, how is it better from average JavaScript development? Dev tools allow you to basically interactively work with your code.
Another detail is that the whole culture of the language, oriented towards immutable data, makes it very easy to evaluate almost any sub-expression in your code, which makes the whole code introspectable in a very playful and dynamic way.
I don't think this is possible with JavaScript.
And you know what? Finally, I realized - I don't have to explain to anyone in exact detail why I have not found the same deep love in C, C#, Python, Javascript, Typescript, Ruby, Go, Java, Kotlin, Swift, Lua, Haskell, and many others. Why do I need anyone's permission to love a tool? I love it, and I love it for many reasons - theoretical, practical, emotional, fiscal.
Sure, I can get behind your excitement for Rust, Kotlin, OCaml, Elixir, Julia - you name it, but please, please do not try to "educate" me about my choices. I don't care about YOUR personal predicaments with Clojure/Clojurescript/Babashka/nbb, even Fennel. You find Clojure not to be worthy of your time - it's YOUR loss. My love for Clojure is not due to MY skill issues, not the result of MY inexperience, not because "I'm in a bubble," or "I don't know any better," or have zero knowledge of type systems, category theory, OOP or design patterns.
Sure, Clojure is not without deficiencies - no tool is ever perfect. Yet pragmatically, no other programming language in the category of general-use PLs today satisfies me more than Clojure, no other language is nearly as joyful to use. I'm sure at some point my stance will change, I will find some other "perfect" language for me, and 100% guaranteed - it will too have some deficiencies and people will be arguing for the merits of choosing it for the job. Until that day, let me just say it again - "I fucking love Clojure!"
The article is excellent and I agree with everything in it.
The stability of the language is unbelievably useful. I look around and it seems it isn't valued in many other ecosystems where people have to rewrite their software regularly. I can't afford to rewrite my app.
There will be plenty of armchair critics here, with cliché knee-jerk reactions (parentheses, JVM, startup time, etc). If you intend to form an opinion, I would suggest you read only into the insightful posts, from people who actually used the language, or from critics who present well thought-out criticism, not just a shallow knee-jerk reaction.
There are good reasons why I said it was only possible with Clojure and ClojureScript. Conciseness, expressiveness, long-term stability, the ability to share business logic code between the client and server, same language used for data serialization (e.g. no JSON), good async code support (core.async), transducers for code reuse and performance, and more.
We bootstrapped for 5 years to well over $1M+ ARR before recently closing a seed round[1], Clojure played a large part in our ability to deliver as a small team. Also in our general happiness as programmers, it is a nice language to work in.
We will grow our Clojure core product team over the next couple of years, but mostly the funding round is about balancing our business to keep up with our product delivery.
Clojure has been very good to me (I had 15 years on the JVM prior to moving to clj/cljs in 2013-ish). YMMV.
[1] https://factorhouse.io/blog/articles/from-bootstrap-to-black...
The problem started when the honeymoon phase ended, and the codebase grew as the business gained traction. Dynamic typing became a burden, and once the key people moved on, they struggled to hire developers who wanted to write Clojure.
Also, JVM juju shooed many away. After I left, a coworker told me they had started rewriting part of it in Go, and that was going alright. Now, their stack mostly consists of Python for LLM stuff and Go for the main backend. There’s still some Clojure running legacy systems that haven’t yet been migrated over.
Also, is running your "hello world" still going to be incredibly slow, or has something changed in the core system (I know I'm supposed to fix that with graails. Or is it babashka ? Or something else, I suppose.)
It's really sad, because i just love the language. Reading about clojure is a pleasure. Trying to write anything has always been a blocker to me, though. Maybe that's the true "immutable" nature of the language ?
For a team that needs to get s** done and has more per employee productivity than Faang combined it's hard to beat the speed with which you can build things when you have the repl and interactive programming.
The jvm while doesn't have great error messages is a fantastic runtime.
Currently https://clojuredocs.org is sort of the go to place. It wins due to Google algorithm.
I wanted to add the ability to vote on answers provided there. So that you have the ability to see the most relevant/popular answer, rather than the oldest.
Raised an issue on the repo, four years ago now. No response:
https://github.com/zk/clojuredocs/issues/222
And why do I mention Google algorithm? Because there was another effort that tried to create a (debatably) nicer documentation, but due to lack of Google traffic, it died. See: https://news.ycombinator.com/item?id=8027119
For a more eloquent argument on its uses, see this blog post:
https://metaredux.com/posts/2019/06/29/farewell-grimoire.htm...
If you can get past the serviceable UI then it's coding nirvana literally looking inside the program
Pair that with a good test suite so you can trigger lots of different scenarios and you're in heaven
But he could have been using F#, OCaml, Haskell. So it might not be just about pure functions and immutable data.
https://youtu.be/nD-QHbRWcoM?si=yneAgMhk6NjTf9ZU
The video I remember when he was describing the origin was on 480i, I’ll try to find it
CIDER (the emacs IDE for Clojure) does a really good job of filtering the stack trace and presenting several views depending on what you care about. L
The only thing that really sucked is just the documentation. They are overly concise. You end up relying on non-official sources of documentation.
I see articles (like the one above) where people are sharing their excellent experience using the language as has been my experience over the years of using it.
A justification would be 'we hate this ugly thing, but we use it because it's cool', but that's not what I've been reading..
There is a lot of elitism around Clojure and all of them are looking to throw down. Even your comment has already garnered 1 of them out of the shadows.
Justify using it against what? Can you name a single other language that does things better than Clojure? Not from your "point of values," but try seeing it from my position. Is there anything that can replace Clojure for me? I love the dynamism, malleability of the language - the flow; writing Clojure programs for me feels like playing a video game - it's plenty of fun. I don't get the same kicks from literally any other "mainstream-ish" language that's being used today. I like Common Lisp, but I have never had any practical, large opportunities to use it seriously, and there are things that I love in Clojure that I'm sure I'll miss in CL. I like Haskell, but it's even less practical (for me).
In my view, there isn't a single language that respects good ideas carried in Clojure and does them in a better or at least pragmatically viable fashion - I've evaluated Elixir, OCaml, Rust and some other choices. I am excited about Jank, can't wait for it to hit the first production-ready release. But today, nothing is more joyful for me to use than Clojure/Clojurescript.
Additionally, while the growing number of compile targets in the Clojure ecosystem is impressive, it can also create confusion for newcomers. Whether it's compiling to native code with Jank, generating quick script snippets with Babashka, or using subsets of ClojureScript like sci or cherry, the diversity is both a strength and a challenge. I believe a concerted effort to unify configurations across these targets would help streamline the ecosystem and make it easier for developers to choose the right tool for their needs.
Also yeah, I agree that Babashka is a delight. I've been using it to manage a Caddy server via Caddy's JSON API and it worked great, super flexible.
FWIW, it works for me — I would not have been able to build and run my business without Clojure. The long-term stability is an especially important trait, under-appreciated by people who do not have a business to run.
Point of my sarcasm here is: you guys invoke them by yourself by writing overly defensive or overly religious posts. Then there will be ofc some critics, as with everything: they do not necessarily express a deliberate scheme against Clojure world. They are just some folks with different opinions, and also: very often wrong ones. But, as an advocate, please don't go into an offended teenager mode, when all the world plots against you; because a lot of rhetoric around defending Clojure sounds like that.
Another is that I get paid to work with distributed systems and databases, and Clojure isn’t even part of the discussion in that sphere. Go, Rust, and the usual Python and Node dominate there, so it’s hard for me to care.
However, I dislike language monoculture and am curious about why people like the things I might not care about. This blog is for those.
I now think of Clojure as a power tool for small shops and consultants. These aren't the kind of places that typically cast a wide net looking for "employees." Most of the opportunities are maybe more focused on proving that you can bring in business and network effectively enough to be a known quantity in the Clojure community.
I worked with Clojure/ClojureScript (mostly ClojureScript) for a couple of years many years ago. It was the first time I'd worked professionally with a functional language, so I made a game of minesweeper in my free time to help get to grips with it: https://github.com/robjinman/cljsmines
Back then, I fully bought into the idea that functional language like Clojure were the future, especially on the web. The way application state is managed is perhaps the key virtue of functional programming - if you get it right, you can design your program to consist mostly of completely pure functions. I remember how enlightening that was once I understood it.
Oh well, I forgot: how to use with TailwindCSS 4? What incantation of build system do I have to configure for live-reloading when classNames changes?
Sorry for the rambling. Just a poor confused user.
This is probably not the easiest "framework" (not sure this is a good name for it) but it was very fun. I think, if I had to progress from just a toy to a really strong, user facing UI, I think it would still be a pretty good choice. If some feature is missing, or something doesn't work as I would have liked, I know it will not be difficult to correct it myself.
I know that this could feel overwhelming, but this is freedom in a world where you expect there exists a single "best practice". As long as the tool is powerful enough it will be fine. On my end, I appreciate the fact there is not a single "web framework" in Clojure. Instead you have tons of libs you can use that work well together because they are all tied with a few common concepts (like ring for example). If you don't like to choose the libs. There are a few people that provide a starter pack with somewhat nice bundle of libs in a single system like pedestal or luminus for only citing two of them.
My recommendation is to not lose too much time looking for the best choice. Most of them will be good.
I wonder what would happen if someone adds a REPL to F#. Sure, it wouldn't have dynamic typing, but isn't static typing good for large code bases?
In particular, judging from the article and comments, it seems that the major boon is having an REPL integrated in the IDE, running in the background, so you can have feedback while you are writing the code.
Having such an integrated REPL for a language like Python would make the productivity up to par with Clojure?
I am trying to understand if Clojure has a particular feature other languages can't add with ease.
Sometimes I just need to get some code running to see a result, which is my main use case for Lisp.
I used to default to Common Lisp, but it needs a lot of scaffolding to become ergonomic, and even then it has too many quirks to be really enjoyable for me.
So I started designing my own:
I mean, you have to try really hard to cultivate a community of people who have a deep love for something, but for some reason can't see what is actually wrong with it that drives ppl away. Passionate users are usually a boon, but apparently not for Clojure. But people somehow seem to build great companies with it. It's strange, and very specific to Clojure culture. I can't help but assume its failure to capture market is tightly tied to its leadership style and/or leader worship.
Anyhow, these are just hot-takes from the margins. I'm sure that passionate users will have something to say, or maybe downvote me.
Common Lisp was the most mind-blowing language I ever touched, and it seems the creator of Clojure really filled the gap between the brilliant simplicity of Lisp as a language and the access to a mature ecosystem. Maybe I should try Clojure as my next language (I am enjoying Rust for now; Rust macros are also cool, but way more complicated)...
1) runs on the JVM, access to any Java or Python library within Clojure without wrappers
2) an immutable-by-default language with a standard library that takes advantage of it
3) the best out-of-the-box concurrency story of any language I know of
4) a very well-developed ecosystem for developer tooling and general project stuff
Common Lisp is very fun, but the stress level is definitely higher with it due to the mutability and the generally less well-designed APIs. Only lists are actually functional, concurrency is YOLO-tier, etc.
I have ADD and I once heard that devs with ADD/ADHD have an incredibly small heap size for context but compensate for their weakness by being great at solving logical problems in that small heap. Types have been essential for me when functioning in code bases. I really struggle with pure JS and untyped Python.
Clojure was similarly hard for me. What tools and/or techniques do such folks use for comprehending already written Clojure code?
Even if I have a smaller total heap size (maybe), personally my hyper focus allows me to nearly dedicate all the heap space to the specific task at hand. I probably outperform neurotypical people here. I just can't have anything else in my head. Task switching kills me.
So it is hard to say cause everyone is a bit different.
For me the interactive, REPL-based workflow makes my ADHD brain very happy. Always having a program running is really nice.
Plus immutability makes it much easier to reason about things.
I do like static typing as well and I could see how it might help. I strongly believe that gradual typing allows for the best of two worlds, so that you can do both exploratory, interactive programming and type driven programming, depending on your needs.
Not sure how well the solutions for gradual typing in Clojure work though. I have only experience with Common Lisp. Coalton might the exactly what you need: https://github.com/coalton-lang/coalton
In addition to that, a real REPL programming really helps to do small tests and understand the code quickly, immutable data structures with data-oriented approach and locally scoped code blocks combined with structural editor are godsend as well.
You mean dynamic typing, I understand.
Clojure (and Common Lisp) is strongly typed, so if you expect type A and you give a value of type B, an error will be raised.
On Common Lisp, which is an interactive development language, you just inspect the stack frame where the error was raised, find the problem, correct the code, recompile your function (while the code is running), and "restart" the stack frame, so the execution continues (without having to restart or redeploy everything and try to replicate the bug). Thus, it is no big deal at all.
On Clojure i'm not so sure how extensive is the interactive support. But there is "spec", which can help.
When you're writing software you benefit from libraries the community made, and lots of people end up writing libraries and contributing back. It's possible Zig is much better for C, but they're failing to gain users due to lots of C developers continuing to use C for new projects, whereas if there were more users maybe the Zig ecosystem would be larger, making it easier to write larger pieces of software.
And I think basically all software is a collaborative effort. Users have investment in tools they think are useful, and frequently contribute or even take over maintenance on critical software. In that case they need to deal with whatever technical decisions that project made. Even commercial software can be abandoned as a buggy mess due to technical choices forcing users to migrate, or eventually open sourced (blender, doom, etc) in which case private technical choices become public. Also I think a lot of people have been forced to contribute (i.e. professionally) to projects that initially made very poor technical decisions and have been scarred by it.
No tool is perfect, and abstract technical debate is awful, but I think it's hard to say there isn't worse and better software, and I think it's worth the effort to put in effort to debate to try to discern and move from worse to better tools since the whole community benefits that way.
Edit: re-essayed
With the right discipline (specs, obsessively normalising all data at the boundaries, good naming conventions) this wouldn't have been a problem, but that discipline is optional, and headbanging aggravation results.
(This is, of course, a generic "dynamic typing" problem, but that's a key feature of Clojure)
I've seen plenty of tomatoes here on the orange site, thrown at Clojure.
Interestingly, years ago, when I was deciding if I should learn Clojure, I have typed into Google "why Clojure sucks". That's actually my usual "research technique" for any new tool - after the initial intro and the wikipedia pages, I typically do that and try to find some criticism, in order to remain level-headed. And guess what? To my surprise, instead, I found a plenty of compelling reasons to learn the damn thing, which I did. That has changed my life, not exaggeratingly, quite for real.
So my conclusion and advice to young programmers? Use whatever tools make you happy. Don't worry about their popularity, don't listen to couch-surfing "experts", try them for yourself. Be skeptical, first and foremost about your own reservations, thoughts, and feelings - something you dislike initially may change your perspective later. Conversely, be looking out for even better ways - remember, it's always possible to find renewed joy for the craft, even after decades of trying different things. There's always something out there that would feel like it was made for you to enjoy it.
The stacktrace-barf has improved a bit in recent versions as I recall, although I've lost the ability to see them by now.
The few folks I know using it are very happy with it, and are very experienced, and talented talented polyglots themselves.
For that reason I'd not rule it out, even if I'm not looking to make a jump or change, but I might try it out.
Golang can make even Python look terse.
There's practically 0 abstractions to meaningfully tame large code bases. You just end up writing same/similar code patterns over and over again.
If you are willing to go down that path I'd recommend using Java entirely. You get the full enterprise thing.
At your clojure job, we’re y’all using spec? I haven’t looked into it yet, and I’m wondering how much it mitigates the annoyances of dynamic types
Spec is about asserting structure at domain boundaries; once you're inside a context, it will not do anything.
i.e. If you're connecting stuff together and you want to make sure the state is correct as you pass between contexts -> great!
If you're writing stuff and internally you miss having a strong type system... uh... I don't think it's really going to help you.
One thing I grew to really love is how small my changes were when I’m just adjusting a decidedly brownfield chain of functions that operate on data. With go in place at work, I added a couple fields to a core data type in my business and I ended up with thousands of lines of changes to pipe them everywhere. Doing the corresponding change in clojure would be <100 lines. I do miss the feeling I get with Haskell that if it type checks, I’m (maybe) good, but like Rich says “List[A] -> List[A] tells me almost nothing about the reverse function.”
If you care about startup time (I don't, my app starts only once), you can use babashka for scripting.
Alternatively, open the project in IntelliJ with Cursive installed or VSCode with Clava or other supported editors and start evaluating code at the repl.
Go through a few tutorials, then go through one of the free books available online. Don't give up when you're stuck.
The language has a steep learning curve because it requires thinking about the program in a different way than many mainstream languages. But it compensates by giving you many 'aha!' moments which make the day :).
All that stuff still works and I don’t think any of them broke their APIs? A lot of people still use lein for example.
You’re talking about new choices coming along, which is a good thing.
Of all the ones you listed, only one is a product of the core team, so it’s not like there has even been “change” on any official level.
If your mindset is, “I need to be on the latest hot thing” that’s about you, not Clojure. You’re allowed to keep building your projects using any of those tools, they still have the same capabilities.
Beware here: it might sound natural to you, but many (and I many _many_) people are working on stacks where "there has been no update to this lib for 6 months" means "the project is discontinuated and has been replaced by something else, it's a waste of time."
So, it might be worth adapting the messaging. For example, there is an awesome-clojure page with a kitchen sink of every possible libs, some of which are dead, and some of which might not be.
Is there a "boring-clojure" page, where, sure, not everything is awesome, but you can be confident that using this lib and that tool and that IDE is going to, basically, "just work" for the foreseenable future ?
[0] Not sure what the correct term is here, but I mean the official default one; I know there are other implementations, and my vague recollection is that there's at least one other one that's either sponsored or maybe even fully developed by Oracle, and that the OpenJDK version is the "canonical" non-enterprise one now anyhow, so saying " first party" feels like it might add more ambiguity than it would resolve
But you're also not recompiling on code change. You're just reloading a function in the REPL. So you only have to load once and then your dev cycle is painless. But the startup time is bad for things like serverless functions unless you use something like GraalVM.
bb is Babashka: a pre-compiled (using Graal), interpreted, scripting language version of Clojure.
In terms of realistic work:
~/workspaces/github/pedestal/tests > time clj -X:test {:in user/eval2321, :line 13, :thread "main", :dev-mode? false}
Running tests in #{"test"}
...
Testing io.pedestal.test-test
Ran 391 tests containing 1180 assertions. 0 failures, 0 errors. clj -X:test 30.15s user 2.04s system 203% cpu 15.838 total ~/workspaces/github/pedestal/tests > java -version openjdk version "23.0.2" 2025-01-21 OpenJDK Runtime Environment Corretto-23.0.2.7.1 (build 23.0.2+7-FR) OpenJDK 64-Bit Server VM Corretto-23.0.2.7.1 (build 23.0.2+7-FR, mixed mode, sharing)
That's on my Intel MacBook Pro. Pedestal's test suite loads a good amount of Java classes and Clojure namespaces.
To make things faster, there's ahead-of-time compilation (which basically captures the read-eval-create-bytecode part of loading a Clojure namespace as Java .class files that can be packaged into your app).
The experience has gotten worse and worse now for a decade. The core team have continued to take things in a worse direction (supported by a small group of fanboys) and most newcomers are now completely baffled by the tooling.
Leiningen attempts to be everything to everyone in terms of building, testing, and packaging Clojure code. It's Clojure's version of Maven.
cli/deps.edn effectively reduces things down to a) what should be on the classpath and b) what should get executed. Working inside an IDE? You just want it to download the dependencies and build a classpath. Running nREPL? Add that to the classpath, and set the starting namespace to start nREPL. Packaging an application? Run dependencies that do that work, based on the clojure.tools.build library.
I was there myself; I used Leiningen and didn't take the time to figure out deps.edn --- until I did, saw the light, and converted all of Walmart's projects to use deps.edn, which greatly sped up our build and improved our dev experience.
Many people still use lein for new projects, especially for larger ones. For small on-off thing, clojure command line is more convenient. So it is a good thing to have more choices.
Without it it’s a matter of time before the codebase can’t be developed anymore and the software doesn’t work as intended.
the fact that because of the repl the happy path is always tested while being built and with clojure spec you can secure the edges, it is quite scalable.
i love static typing and rust in general but with web applications, the problem space is really a distributed concurrency problem of dynamic data. and clojure is basically tailor made for that.
Pointers and null values are inescapable in go, and people treat them worse than they do in C. Generics are too limited (I can’t define a scala flatmap like function) And I can’t make data immutable or control access in any fine grained way in go. Compile time in my go monorepo is minutes now. Someone added a bunch of go lint rules.
Being simple is not a good thing. A simple language means that the programs become complex and hence complicated. The advantage is that it's easy to get up to speed and to write greenfield code though.
This is how I use Python to be honest. If I'm unsure of something, I just bust out the REPL.
I do not know how you do that in python without using a debugger and stepping through the code line by line. To be fair I used Python only a little bit almost a decade back. Are things more interactive now?
Spec is still alpha and I’m not sure it will evolve more or if it’ll be something completely different. At least they’re not pushing you down the wrong path. Use/look at Malli instead of spec.
You can't just pass in a Clojure `fn` into a Java lambda function. You have to `reify` the interface and implement the single method. It's annoying and verbose and frustratingly the equivalent code is considerably cleaner in Java as a result.
I know that this is a product of how Java implemented lambdas by having interfaces with a single method, and I'm not saying that it would be trivial to add into Clojure, but I don't think it's impossible and I think people have been complaining about this for more than a decade now.
So while I love Clojure, it's probably my favorite language, I do get a little annoyed when people act like it's "stable" because there's nothing to fix.
No, but the release of spec single-handedly killed Schema, a more mature contract lib.
Then spec stagnated, because the core team didn't want to work on it, but as a core team library, nobody else can work on it, either.
Thus was born Malli, because people got tired of waiting.
I see the fact that there isn't much happening with the language as a positive myself. I want the core language to be small and stable. The big difference with Clojure and most languages is that it makes it far easier to implement new semantics in user space. It has a powerful macro system and new ideas can be expressed using libraries instead of having to be baked into the core language itself.
Most language grow through accretion as usage patterns change over time. New end up features end up getting bolted on to facilitate that, and the language keeps growing. The problem here is that the scope of things people have to know keeps growing as a result.
Meanwhile, Clojure managed to stay small and focused, while different kinds of ideas are expressed using libraries. When a new idea comes along people can just use a new library, and they don't have to learn about all the previous libraries that worked differently. I think this is a huge benefit from user perspective.
I mean, Datomic is free to use!
[core.async.flow]: https://github.com/clojure/core.async/commit/03b97e0b3e0ec32...
1. Syntax. It's a bit easier to just evaluate a symbolic expression (put in a simplified manner: anything enclosed by parentheses) than to select a region of Python code to evaluate. But it's no major issue. Don't know about any syntax that has similar properties, Ruby seems to be somewhat similar in spirit, every expression returning an object. Python OTOH has all kinds of constructs you can't evaluate readily.
2. Functional code. Lisp code is generally largely side effect free, Python is not. That goes from the libraries down to how you write your own code by convention. Evaluating code without side effects for quick feedback is a breeze. For side effect riddled code, it's kinda easier to just use unit tests with mocks etc. for that. There are lots of languages with good functional libraries nowadays.
Generally speaking, the things you list are what used to be unique about Lisp. But apart from the two points I'm making, I don't believe that's the case anymore. Lots of languages got inspired in the last few decades. I love Lisps and I wish I could use them more, but I'm happy lots of popular languages got reasonably close.
F# does, in fact, have REPL and excellent one at that!
There is F# interactive and F# scripting. It's my workhorse for all sorts of automation and quick "analyze data and draw a chart" style of one-off scripts. It's vastly superior experience because when editing F# scripts you get full LSP, ability to reference nuget packages without vendoring anything, etc etc.
Gradual typing and full type inference make it look at times like dynamically typed language but it's anything but and gives really nice user experience.
https://learn.microsoft.com/en-us/dotnet/fsharp/tools/fsharp...
After all, an elitist can also sometimes be right.
Disclosure: I say this as someone who has been about as deep as you can go in the Clojure community.
I really like this framing of "in tension". Multiple things can be true. I think it represents a healthy tension that I'm grateful exists, even if I'd not personally wish to be operating in that specific tension. But I'm sure the people living in are different than me, and can do things in this world that I could not :)
It's a large part of why I've stepped back from clj these days.
Did you encounter some hostility?
Elm is another example
How was your experience in working with the existing Clojure codebase?
The codebase was democracy-related civic tech software. I was a bit biased against Clojure because I wished for the world to show up in the codebase, but it was written in an uncommon language that scared away most every coder who showed up with curiosity.
Probably just the wrong language for the culture I wished to exist around the software. Plus the maintainers used Rich's essays to emphasize their desired ethos of "singular vision" development, which I admittedly became resentful of.
When I do CL, I try to code it in a more immutable style and avoid things like self until I get my own version bootstrapped.
If the thing was in Java, each fix attempt would mean waiting for startup and state re-creation. And each successful debug could have meant multiple sessions (vs visiting any mix of spots in a single recording)
i don't know about nowadays though, he probably has graduated long ago!
nowadays i'd just use core.spec/malli/other honestly (for what i use clojure for).
I'm not sure what a programming language has to do with distributed systems and databases, but as another data point, I happily work with distributed systems and databases, using Clojure :-)
(see also https://jepsen.io which I would very much call a "part of the discussion in that sphere", written in Clojure)
As to databases, there are a fountain of databases emerging from the Clojure ecosystem: Datomic, Datascript, Datalevin, XTDB, and friends aside, there is Rama, which is a distributed database (or to replace database?)
Are there any tricks or habits you learn with the REPL that go beyond what a static type system gives you?
SMH.
If you already like Common Lisp, ABCL (Armed Bear Common Lisp) is a mature fully ANSI Common Lisp-compliant implementation, that runs on the JVM and can instance or load Java classes or call Java methods without wrappers and in only one line of code.
> the best out-of-the-box concurrency story of any language I know of
Lots of concurrency models also available on Common Lisp, including async, channel-based, etc.
> Only lists are actually functional, concurrency is YOLO-tier, etc.
I'm sad you had that experience, but there is tons of solid concurrency support in Lisp, for example lparallel is awesome.
Clojure is nice as a JVM language alternative, that is, better than Kotlin, Groovy, etc.
However the loss of the great interactive development facilities that Common Lisp has is a major, major downside. Interactive development is what makes CL (and also Smalltalk/Pharo) great.
I disagree Common Lisp has a better development environment, I haven't found that to be true.
But use whatever makes you happy!
This is how I wrote unit tests when I worked on Mathematica: try out every edge cases of the function in a notebook, and then use a tool to extract all the input/output cells and convert them to tests. I didn't know the term reify for this practice, I like it!
When you make a concrete subclass of an abstract class, you are "reifying" that class. When you made the abstract class from the concept of something, you are "reifying" that concept.
It's a fun word.
His comment walks you through what it's for. The basics: configure your flow of data from producer to queue to consumer and so on as data, in one place.
It was not a language "of the people". And not just in how uncommonly known it was, but even the philosophy in how the language governed itself. It felt harmful to me: the cross-pollination between the ethos of the Clojure project (via the well-intentioned developer who brought pieces of Clojure culture into conversations and discussions) and the democracy project.
For complex data structures I typically just hide details behind a protocol and deal with the set of interface functions.
You can then freely mess with the internals and not worry about details
Similarly nothing prevents invoking missing functions of a protocol, you will only know during runtime.
BEAM handles concurrency better than the JVM.
Elixir threads are implemented using private memory where Clojure uses public memory.
Mix is superior to Clojure’s tooling.
Elixir is easier to learn and write.
OTP is fantastic.
Phoenix is an excellent web framework.
Java would be another.
But does it really matter now days? If you're doing anything web oriented, you don't get to abstract the hot mess that is web programming away. You still have to learn a ton of nuanced stacks, many couched in idioms that were established decades ago, and perservere today, wrapped in layers of whatever.
It's better now, but people are pretty uncritical of the core team, and adopted tools.deps before its surrounding ecosystem was ready to fully replace lein.
The upshot was, everyone got to rebuild the wheel for a couple years. And I say this as someone who once lost a week debugging a subtle lein/maven classpath AOT bug.
One of my pet peeves on github is with modern ecosystems that people ask after 1 week of no commits 'is this project dead'; we use rocksolid well documented and robust libraries that have basically not been updated for a decade; apparently you dont need to change the api every 2 days completely and introduce breaking changing in minor versions of libraries where the actual thing they have solved hasn't changed since the dawn of computers themselves... Like is the normal in npm libs for github star vying.
And the supposed lack of libraries; sure it happens, but more often it is so easy to roll (99% of npms) that in CL no one would use the CL lib if their was one; faster to roll than search for one.
I agree with the modernisation steps though: need to bring together fairly recent enough industry users and scan their code base for which common lisp functions and libs are used (as for as I see, most practical people use a subset and don't often go look for different ways to do something they might be buried in hyperspec somewhere) and distill that down to a Industry Common Lisp 'spec' and modernise everything related to that.
Don't underestimate the amount of information even simple type signatures contain!
f :: [a] -> [a]
f [] = []
f (x:xs) = x: x: f xsHickey: I value X, Y, Z
Me: Yeah man!
Hickey: We get great consequences A, B, C
Me: Ah yeah, I love programming like that. That's why I love Haskell!
Hickey: That's why Haskell is bad.
Me: err, what!?
> It tells you that the function must calculate a subset of a permutation of the input list
As tromp pointed out, "permutation" is technically incorrect. You mean something like "a list formed only from elements of the elements of the input list, and the particular arrangement is independent of the values of the input list"!
Not sure why you were downvoted though.
I didn’t say Haskell was bad though. I love Haskell in a different way from clojure.
Here is Rich’s work on the subject: https://youtu.be/YR5WdGrpoug?si=7C8EjQ7TVo2Ua8w7
I’m biased, but I think he’s got a point.
(defprotocol P
(method [this ^Integer i]))
(extend-protocol P
String
(method [s i] (.substring s i)))
both (method "test" "call") and (method 1 2) will be accepted by the compilation phase but will fail at runtime.Of course there's no requirement for Clojure code to be AOT compiled anyway so in that case any name errors will still only be caught at runtime when the compilation happens.
Type hinted bindings are only converted into a cast and are not checked at compilation time either e.g.
(defn hinted [^String s] (.length s))
(hinted 3)
will be accepted but fail at runtime.deftype is only used for Java interop an is also not a form of type checking. The methods will be compiled into Java classes and interfaces, but the implementations defer to regular Clojure functions which are not type checked. You can only make use of the type information by referencing the compiled class files in Java or another statically typed language, using them from Clojure will not perform type checking.
It's a completely different thing to be coding "from within your running program" and it's hard to signal that to who has never tried.
When I've seen it done, it's seemingly executed in-place, which is very cool.
The goal isn't to introduce .. a compilation step? but to have the program blow up in the spot where there is an type mismatch. If you don't use a protocol you may not blow up, you may generate a nil, and you may blow up much further down the line (or not at all)
In the rare instances where dynamic types cause problems, they're virtually always something convoluted like that. The protocol design pattern describes the interface and protects you from hard to debug situations
For most people Python/Javascript also does the job and you don't need to learn another paradigm — a Lisp — to code for it, which also makes sense.
However, learning a Lisp also makes you a better coder because of immutability and less side-effects. Hence why Clojure is still around.
True, and people should use whatever works best for them/for the job, no questions asked.
But they also have nowhere near the same experience even though they technically have REPLs. The way a JS/Python dev typically use a REPL is experiment in the REPL itself, then when happy, write the "real code", while a Clojure developers write "real code" all the code, sending selections of code to a REPL in the background, viewing the results in their editor, and just saves the file when they're happy. It might sound similar, but very different experience.
> However, learning a Lisp also makes you a better coder because of immutability and less side-effects. Hence why Clojure is still around.
I don't think "immutability" and "less side-effects" is something lisps in general/all lisps promote/facilitate, it's mostly a thing that Clojure (and children) sticks out for caring a lot about. Scheme/Common Lisp is about as immutable as JavaScript is, and lots of CL programs/code out there spreading mutation all over the place, while in Clojure it's pretty common to just have small "pieces" of mutation in somewhat centralized location.
It can do, but it also can make you a worse coder. Specifically in typed languages.
One of the issues I've ran into with Clojure devs doing Java is that instead of relying on a type, they tend to want to write stuff like `Map<String, Map<String, Object>>`. Even when the key sets in both maps are well known.
This becomes worse when you mix that with Immutability. Immutability can be fine except when you need a mutation. In applications that require heavy mutation of data a `Map<String, Map<String, Object>>` is one of the worst ways to represent structured data, copying that structure is EXTREMELY expensive.
This isn't to say that you shouldn't usually strongly prefer Immutability. But it's also to say you shouldn't underestimate the cost of allocations and copying data.
Theres always tradeoffs. Part of being a good programmer is knowing when those tradeoffs are best applied.
Around these parts it's common to have read and appreciated Paul Graham's old writings about becoming a better software developer through Lisp, and that would be Common Lisp, i.e. very mutable, commonly object oriented.
But this is because JSON is an untyped data structure. (And btw, a flawed one...)
You would have this problem in any programming language.
Statically typed languages reduce the need to know how the data is structured or manipulated. The market has clearly chosen this benefit over what Clojure can provide.
With static typing, you are doing specification and optimization at the same time, which is maybe necessary because compilers and languages are not sufficiently smart but also because of this mix it complicates reasoning about correctness and performance. Also static typing introduces a whole universe of problems with itself. That's why we have reflection or stuff like memory inefficient IP address objects in Java:
For a simple IPv4 address normally representable using 4 bytes/ 32 bits Java uses 56 bytes. The reason for it is Inet4Address object takes 24 B and the InetAddressHolder object takes another 32 B. The InetAddressHolder can contain not only the address but also the address family and original hostname that was possibly resolved to the address.
For an IPv6 address normally representable using 16 bytes/ 128 bits Java uses 120 bytes. An Inet6Address contains the InetAddressHolder inherited from InetAddress and adds an Inet6AddressHolder that has additional information such as the scope of the address and a byte array containing the actual address. This is an interesting approach especially when compared to the implementation of UUID, which uses two longs for storing the 128 bits of data.
Java's approach is causing 15x overhead for IPv4 and 7.5x overhead for IPv6 which seems excessive. Is this just bad design or excessive faith in static typing combined with OOP?
But that's not what a Clojure dev would do.
1) We use Malli [0] (or similar) to check specs and coerce types if needed at every point. Checks can be left on in production (I do), or disable–up to you.
2) If the coercion is difficult, use something like Meander. [1]
3) If even that isn't straightforward and you need actual logic in the loop, use Specter. [2]
4) If you're not sure what going on at intermediate steps, use FlowStorm [3].
5) But you're going to be processing a lot of data you haven't seen before! Use, Malli with test.check [4] and make use of property-based testing with generators.
None of this is "advanced" Clojure, this is bread-and-butter stuff I use every day.
6) Need a Notebook-like experience to get better visualization of intermediate data? Use Clerk [5].
7) Need special checks on API usage within your codebase? Use clj-kondo [6] with custom linters. They're less than 10 lines each.
Unlike default-mutable languages, or typed, it's safe and easy to use libraries with Clojure and they tend to have very little churn. Total opposite from Python or JavaScript (if you're used to that).
It's almost impossible to give the impression of what it is like to develop with Clojure if you've only ever used languages with static typing, or languages from the Algol family.
Honestly, I hated Clojure's syntax at first BECAUSE I COULDN'T READ IT, and I loathed "structural editing." After 2-3 weeks, I read it just fine and it's hard to remember I ever couldn't do so. Now I like it, and structural editing makes it so easy to change your code, I couldn't live without it at this point.
Basically, all my "fears"/dislikes were unfounded—it was a skill issue on my part, not a problem with Clojure.
[0] https://github.com/metosin/malli
[1] https://github.com/noprompt/meander
[2] https://github.com/redplanetlabs/specter
[3] https://www.flow-storm.org/
To a first an approximation, every Rust project uses Cargo and every Elixir project uses Mix, and they are both first-party tools so they have a level of stability, approachability, and ubiquity that comes with being tied to the language. I’d kill for Clojure to have something half as good as Cargo or Mix and have it be the only build tool, but that ship has sailed.
Which is all the weirder : rich hickey could (should ?) have figure that "building infra" was one of the aspect where clojure was a great fit ("lein as an ant+maven killer.") To be fair, it wasn't as much in the zeitgeist at the time.
Still, a new language nowadays must have, on aday one, a package manager and a linter and a formatter and a test runner, etc... - just like it must have an LSP.
It's funny looking a zig trying to fight this.
It's funny watching roc reinventing that weel.
I guess jblow will rationalize against it for jai.
I wonder if someone is going to standardize "the rest" of the toolchain. ("Language dependency manager protocol", anyone ?)
Do you have some source for that? Perhaps an example?
There's a bit of ceremony but similar to how you can interface with Java libraries from Clojure you can pull in Erlang when Gleam isn't enough.
You can do some code shortening with use, https://erikarow.land/notes/using-use-gleam, but it will likely always be rather chatty and won't support metaprogramming: https://lpil.uk/blog/how-to-add-metaprogramming-to-gleam/
So if you use defprotocol and deftype for every domain objects in your code, your code won't compile if there's a type error. Try it.
BTW, that's the way many Clojure libraries are implemented. These libraries rely on dispatch on type to work, so they are taking advantage of the type checking.
Of course, you will say, "oh, clojure is not normally AOT, so it's not dong the checks.", but that's another issue. The issue at hand is this: can you write Clojure such that types are checked at compile time. The answer is YES.
The compiler may run only when you run the program, that's a different issue. You are confusing these two issues.
If you want a separate compile stage, then basically you are already excluding runtime compilation, i.e. you are arguing against runtime compilation. So it's not really about typing, but about how you want to run the program. Isn't it? You want AOT for everything, you don't want runtime compilation. That's it. It has nothing to do with types.
Clojure functions are compiled into implementations of clojure.lang.IFn - you can see from https://clojure.github.io/clojure/javadoc/clojure/lang/IFn.h... that this interface simply has a number of overloads of an invoke method taking variable numbers of Object parameters. Since all values can be converted to Object, either directly for reference types or via a boxing conversion, no type checking is required to dispatch a call. With a form like
(some-fn 1, "abc", (Object.))
the some-fn symbol is resolved in the current context (to a Var for functions defined with defn), the result is cast (not checked!) to an instance of IFn and the call to the method with required arity is bound. This can go wrong in multiple ways: the some-fn symbol cannot be resolved, the bound object doesn't implement IFn, the bound IFn doesn't support the number of supplied arguments, the arguments are not of the expected type. Clojure doesn't check any of these, whereas the corresponding Java code would.Protocol methods just get compiled into an implementation of IFn which searches for the implementation to dispatch to based on the runtime type of the first argument, so it doesn't introduce static type checking in any way.
You guys make it out like Clojure is doing something extra to hide Java types, but it doesn’t. What Clojure does is really minimal on top of Java. It barely hides anything.
If you give it type, it will check type. If you don’t give a type, it falls back to a default type, Object, which IS a TYPE. The fact that Clojure compiler cannot deal with GraalVM SVM Pointer type tells you that it’s checking type, because Pointer is not an Object! I found this out the hard way: https://yyhh.org/blog/2021/02/writing-c-code-in-javaclojure-...
“One limitation that one needs to be aware of when writing native image related Clojure code, is that most things in the GraalVM SDK inherit from org.graalvm.word.WordBase, not from java.lang.Object, which breaks the hidden assumption of a lot of Clojure constructs.”
Here's one such project: https://github.com/swank-js/swank-js
I'm sure it's a difficult thing to implement, so I'm a little forgiving, but considering that Java interop is one of the biggest selling points for Clojure I do think it's fair to criticize a bad experience with it.
I need to play with the newer stuff though; the linked changes seem cool as hell.
But this long list of runtime libraries is definitely a downside of Clojure. It's people trying to grapple with things mostly solved with static typing where you can just write a(b(c())) and it fails before it hits your fancy yet-another-thing-to-learn Malli library in runtime.
They might be great libraries, but you're only seeing one side of the trade-off.
I learned Emacs with evil-mode, paredit, nrepl/cider, and Clojure in my early 20s and used them for six years, and I was pretty gung-ho about it like you. But eventually I started using static typed languages for work and decided that I couldn't go back. It's like trying to read Javascript after you've spent five years with Typescript. You just think "wow, I can't believe I did that for so long."
And I'm remembering times I've used paper and pencil to figure out how map is being transformed as it's passed through library code. I don't miss that.
The Value of Values: https://www.youtube.com/watch?v=-I-VpPMzG7c
Using types to model data is a terrible idea.
Apparently I and my fellow Clojure devs aren't real Clojure devs. Or perhaps you mean "true" clojure developers, or "good" clojure developers. (cf. https://en.wikipedia.org/wiki/No_true_Scotsman)
And even if we were Clojure devs we've inherited multiple big Clojure codebases that were apparently written by non-Clojure devs, and heavily refactoring is not on the to-do list.
What on earth?? Who are these people, this is the first I've heard of this and am having trouble understanding what the argument could be. Like, basically all languages get transformed to an AST as part of the compile step soooo I guess any language is sort of a lisp.
"Take a Lisp program, indent it properly, and delete the opening parens at the start of lines and their matching close parens, and you end up with something that looks rather like a Python program."
also Peter Norvig on HN: https://news.ycombinator.com/item?id=1803815
(defn f [^String s] (.length s))
(f 3)
is a valid Clojure program that fails at runtime with a cast error. class X { public static int f(String s) { return s.length(); } }
X.f(3)
is not a valid Java program at all. Clojure compilation generates bytecode to dispatch dynamically and all but the most basic checks are handled at runtime by the JVM. This is fundamentally different to the static type checking that languages like Java and Scala do. It's not that Clojure is hiding something from Java, but rather that it isn't doing the considerable amount of effort the Java type checker does to analyse the program before execution. This is by design - Clojure has deliberately avoided adding a static type system in favour of things like spec.