Clojure is a lot of fun to tinker with, but man… I love my static types. I think I’d hate to work on a large codebase in Clojure and constantly be wondering what exactly “m” is.
As a programmer with almost exclusively statically-typed compiled language experience, I used to strongly believe this too. In the last year, though, I've seriously toyed around with a number of dynamic languages, really trying to grok the dynamic mindset, and it's been very eye-opening. I never expected quite the degree of productivity boost that I have felt in these languages, and I must admit that I've also found many of them quite joyful to work with. Dynamic languages are profoundly creative, in all the senses of creative. I've found myself thinking about my programs in a totally different way, which has been really lovely (and honestly a timely reminder of what I loved about programming in the first place).
To be fair, I will readily say that the lack of static analysis really does bite when refactoring, though I think that good design principles and the overall productivity multiplier may offset that cost (also unique, descriptive, grep-able names!). I guess I've also seen enough C++ template spaghetti to know that static typing is no panacea either.
I don't know to what extent I'll use dynamic languages going forward, though for now I'm kind of in love with opening up a window into the computer and building up my digital sandcastles. Many of these languages also have a great FFI story, which is making me dream up cool bilingual projects that play on the strengths of both approaches.
All in all, no regrets about my adventures in dynamic-land.
> Clojure is a lot of fun to tinker with, but man… I love my static types.
Static types are great, but boy... I love my REPL. I think I'd hate to actually work while writing code. REPL-driven interactivity with Clojure allows me to treat the work like I'm playing a video game.
Have you actually ever used a Lisp REPL? Lisp REPLs are different. If you get down to some pedantic level, every stage there - Read, Eval, Print and Loop, are all slightly different. Compared to a simplest, most basic Lisp REPL, any sophisticated REPL in non-lispy languages simply feels like an interactive shell, nothing more. so called "REPL-driven development technique" allows you to build programs interactively - as I said: "it feels like playing a video game" - you can send any expression or sub-expression, almost without any preceding ceremony, directly from your editor, where you'd be typing code. You evaluate things, sending them to a REPL instance. That REPL instance might be in your browser, or some pod in a kubernetes cluster, or a spaceship, million miles away - that's not some science fiction: https://www.corecursive.com/lisp-in-space-with-ron-garret
Homoiconicity allows code structures to be treated as what they are, and sending them to the REPL without any kind of transformation. So, yes, while technically, those langs also have REPLs, in reality, writing code doesn't really give you the same kind of feeling.
It is my favourite language on the JVM, when not using Java.
Because of being a Lisp based language, bringing something else to the table besides "lets replace Java", and the community being welcoming of the host environments where Clojure is a guest.
Clojure is not only on JVM. I often use babashka for shell-scripting and nbb for tinkering on node.js. There's ClojureDart if you like Flutter. For Lua, there's Fennel which is not Clojure but has similar syntax and inspiration. There's Clojerl for Erlang, and glojure, joker and let-go for Golang. There's clj-python and clojure-rs. There's jank-lang (which is not production-ready yet, but already is very promising).
It's not just that. Static types do help, yet dismissing an entire language because of a single aspect of it is extremely short-sighted. It's like rejecting Russian or Turkish, only because they have no concept of definite or indefinite articles.
Sure, Clojure is dynamically typed, but it is also strongly typed. That in practice means that for example Clojurescript when compiling to Javascript enforces those type guarantees, sometimes emitting safer code than even statically typed Typescript cannot.
That is exactly my feeling, like lately everything must be “safe” and statically typed. While I do see some (big, sure) pros I see also some cons that I feel are systematically ignored or neglected. For me it seems to be a kind of fade/hype… but maybe I’m just connected to the wrong news feeds.
> maybe I’m just connected to the wrong news feeds
I don't think so. It's just like said in another comment:
For everything, there's a trade-off. Some just accept those trade-offs, build their vision, and launch it into the world; Some waste time, lamenting that reality doesn't align with their ideals.
Static typing works - just like formal methods, just like dependent types, just like unit testing, just like generative testing, just like many other different ideas and techniques. They each have their own place and use cases, strengths and weaknesses, pros and cons. Picking one single paradigm, technique, design pattern, or methodology - no matter how amazingly powerful they are - and just dogmatizing and fetishizing it is simply immature. Reaching the point where you clearly understand that there are truly no silver bullets and everything is about making compromises is a sign of professional growth and true, genuine experience.
For us the combination of malli and clj-kondo worked really well. Also we haven't faced that problem yet, as the codebase is fairly small. But I can totally see when types become quite useful when navigating large codebases.
Having worked with large Clojure codebases I found the Malli/Spec duct-taping of types to be a poor man's statically typed language, especially with the developer experience being quite poor. While it will runtime validate, I still have no idea what shape anything is by just hovering it - having to constantly navigate between the definitions file, and they are also more cumbersome to use and maintain.
I've come to the conclusion that it is just a better experience using a language that already has static types for large projects, than trying to make a dynamic language have similar things. Having to wrap every function in a error boundary to get somewhat of a meaningful debug experience is just .. awful.
> duct-taping of types to be a poor man's statically typed language
On the other hand, they allow you to do some very interesting things like using specs for complex validation. Once written specs can be then used for generating data for both - UI testing and property-based unit-tests. We once have build set of specs to validate an entire ledger - imagine having to be able to generate a bunch of transactions where numbers intelligently build based on previous entries.
Other languages even though have similar capabilities, like type providers in F#/OCaml, zod in Typescript, quckcheck/scalacheck in Haskell & Scala - Clojure is quite unique here in combining runtime validation, generative testing, and data definition in such a cohesive way. The ability to compose specs and use them across different contexts (validation, generation, documentation) is particularly powerful.
Another impressive thing is that you can easily share the logic between different runtimes - same specs can used in both - JVM and Javascript, which surprisingly difficult to achieve even when writing in Node with TS/JS - you cannot easily share the same validation logic between backend and the browser, even for the same javascript runtime, using its native language. Clojure lets you do that with ease.
For everything, there's a trade-off. Some just accept those trade-offs, build their vision, and launch it into the world; Some waste time, lamenting that reality doesn't align with their ideals.
Yes, you would know what it is at that moment. You would not however know if what it is at that moment in time is actually correct, or what the expected shape is, without deconstructing the entire function that is the receiver of the data. That's where static types are useful - I can just hover my mouse over a function and it will show me what is the expected input, and expected output, and I do not need to read and understand the contents of the function to know if the data is correct, because it would throw an exception if it is not, like if a string suddenly becomes a number or is missing a piece of information, etc.
Theoretically, yes. And trust me, I loved static types. I use Rust for almost everything. However, the programming loop or the iteration loop that you get into with Lisp, especially with things like Common Lisp, it's not that much of a concern. But I agree with any other language which is not a Lisp, a static type system is far superior.
As nice as nrepl/cider are, doing what amounts to setting a breakpoint in the middle of a function to see what `m` looks like isn't a replacement for knowing the type without executing code. It's just something we put up with.
yeah as I also commented on the sibling comment the real thing here is that the way you program a clojure application or a common Lisp application especially because I used to use Steelbank common Lisp so I can talk about that is you immediately go into the REPL and you jack in it's actually a little bit difficult in clojure to do that compared to common lisp.
But the mental model is fundamentally different. It's not like you write a bunch of code, set a breakpoint and see what things are. You essentially boot up a lisp image and then you make changes to it. It's more like carving out a statue from a piece of rock rather than building a statue layer by layer.
I've been using Clojure for a while and I rarely ever wonder "what 'm' is" - that almost never happens, despite the language being dynamically typed.
Data shapes in Clojure typically explicit and consistent. The context usually makes things quite obvious. Data is self-describing - you can just look at a map and immediately see its structure and contents - the keywords serve as explicit labels and the data, well... is just fucking data. That kind of "data transparency" makes Clojure code easier to reason about.
In contrast, in many other PLs, you often need to know the class definition (or some other obscured shit) to understand what properties exist or are accessible. The object's internal state may be encapsulated/hidden, and its representation might be spread across a class hierarchy. You often can't even just print it to see what's inside it in a meaningful way. And of course, it makes nearly impossible to navigate such codebases without static types.
And of course the REPL - it simply feels extremely liberating, being able to connect to some remote service, running in a container or k8s pod and directly manipulate it. It feels like walking through walls while building a map in a video game. Almost like some magic that allows you to inspect, debug, and modify production systems in real-time, safely and interactively, without stopping or redeploying them.
Not to mention that Clojure does have very powerful type systems, although of course, skeptics would argue that Malli and Spec are not "true" types and they'd be missing the point - they are intentionally different tools solving real problems pragmatically. They can be used for runtime validation when and where you need it. They can be easily manipulated as data. They have dynamic validation mechanisms that static types just can't easily express.
One thing I learned after using dozens of different programming languages - you can't just simply pick one feature or aspect in any of them and say: "it's great or horrible because of one specific thing", because programming languages are complex ecosystems where features interact and complement each other in subtle ways. A language's true value emerges from how all its parts work together, e.g.,
- Clojure's dynamic nature + REPL + data orientation
- Haskell's type system + purity + lazy evaluation
What might seem like a weakness in isolation often enables strengths in combination with other features. The language's philosophy, tooling, and community also play crucial roles in the overall development experience.
If one says: "I can't use Clojure because it doesn't have static types", they probably have learned little about the trade they chose to pursue.
Correction: It's not that you "don't have to use the REPL"; you simply cannot even have it in that case. REPL-driven development is quite a powerful technique, and no, "many other languages too" don't have it. For it to be "a true REPL," it must be in the context of a homoiconic language, which Clojure is.
Sure, static typing is great, but perhaps you have no idea what it actually feels like - spinning up a Clojurescript REPL and being able to interactively "click" and "browse" through the web app programmatically, controlling its entire lifecycle directly from your editor. Similarly, you can do the same thing with remote service running in a kubernetes pod. It's literally like playing a video game while coding. It's immensely fun and unbelievably productive.
With a REPL-connected editor (and most have a way to do this), you can simply hover over it in your editor as well. Even though most languages can have a REPL today, few integrate it in the development experience the way lisps do.
The compiler should know it for you, so you cannot get it wrong no matter what. The REPL here is a band-aid not a solution.
I mean, I love Clojure, and used it for personal and work projects for 10+ years, some of which have hundreds of stars on github. But I cannot count the time wasted to spot issues where a map was actually a list of maps. Here Elixir is doing the right thing - adding gradual typing.
> But I cannot count the time wasted to spot issues where a map was actually a list of maps.
Sorry, I'm having hard time believing that. I don't know when was the last time you've used the language, but today there are so many different ways to easily see and analyze the data you're dealing with in Clojure - there are tons of ways in CIDER, if you don't use Emacs - there are numerous ways of doing it in Calva (VSCode) and Cursive (IntelliJ), even Sublime. There are tools Like Portal, immensely capable debuggers like Flowstorm, etc. You can visualize the data, slice it, dice it, group it and sort it - all interactively, with extreme ease.
I'm glad you've found great fondness for Elixir, it is, indeed a great language - hugely inspired by Clojure btw.
You still don't need to bash other tools for no good reason. It really does sound fake - not a single Clojure developer, after using it for more than a decade, would call a Lisp REPL "a band-aid and not a solution". It smells more like someone with no idea of how the tool actually works.
Maybe it's so. Or maybe you run my code in your deps. As you can see, there is at least one Clojure dev who thinks so.
I found spec very useful and damn expressive (and I miss it in other languages), but again that's runtime. I know Rich says such errors are "trivial", but they waste your time (at least mine).
To each their own. Some people (not me) say that Rust's pedantic compiler feels like bureaucratized waste of time akin passing through medieval Turkish customs. For me personally, working with Clojure dialects feels extremely productive. Even writing in Fennel, which is not Clojure, but syntactically somewhat similar is much faster for me than dealing with Lua. Even when I have to write stuff in other PLs, I sometimes first build a prototype in Clojure and then rewrite it. Although it sounds like spending twice the effort, it really helps me not to waste time.
What do you mean? Clojure is strongly typed language - every value always has a definite type. It's not like Javascript. Types in Clojure are fixed and consistent during runtime, they just aren't declared in advance.
Do you think there's only one path to your function? There could be thousands in a big system. The type of the value you'll get will depend on the path you call it from. Even if it's only one path, you could easily have code doing stuff like this:
if x > 10:
call_my_function 10
else:
call_my_function "foo"
Can't you see that unless you test every path, you won't know what type you will receive??
Your contrived example is a bad smell in ANY language. No sensible coder ever writes a function that accepts both numbers and strings - handling multiple types should be done through proper polymorphic constructs, not arbitrary conditional branches.
There's a wide spectrum of correctness guaranties in programming - dynamic weak, dynamic strong, static, dependent, runtime validation & generative testing, refinement types, formal verification, etc.
Sure, if your domain needs extreme level of correctness (like in aerospace or medical devices) you do need formal methods and static typing just isn't enough.
Clojure is very fine, and maybe even more than just fine for certain domains - pragmatically it's been proven to be excellent e.g., in fintech and data analysis.
> Can't you see that unless you test every path ...
Sure, thinking in types is crucial, no matter what PL you use.
And, technically speaking, yes, I agree, you do need to know all paths to be 100% certain about types. But that is true even with static typing - you still need to test logical correctness of all paths. Static typing isn't some panacea - magical cure for buggy software. There's not a single paradigm, technique, design pattern, or set of ideas that guarantee excellent results. Looking at any language from a single angle of where it stands in that spectrum of correctness guaranties is simple naivety. Clojure BY DESIGN is dynamically typed, in return it gives you several other tools to help writing software.
There's an entire class of applications that requires significantly more effort and mental overhead to build using other languages. Just watch some Hyperfiddle/Electric demos and feel free to contemplate what would it take to build similar things in some other PL, statically typed or whatnot. https://www.youtube.com/watch?v=nEt06LLQaBY
What are you on about, I didn't say anything about panaceas or what not... just said the obvious: in a language without static typing you just can't assume the type of the argument at all.
> And, technically speaking, yes, I agree, you do need to know all paths to be 100% certain about types.
> myriad reasons why Common Lisp is far superior to Clojure
Some narrow view. Have you tried thinking that maybe Clojure intentionally chose not to include type declarations because they can lead to a messy middle ground? After all, maybe not every feature from Common Lisp needs to be replicated in every Lisp dialect? Besides, Clojure's Spec and Malli can be far more powerful for validation as they can define complex data structures, you can generate test data from them, you can validate entire system states, and they can be manipulated as data themselves.
If CL so "far superior" like you say, why then it can't be 'hosted' like Clojure? Why Clojure has Clojurescript, ClojureCLR, ClojureDart, babashka, nbb, sci, etc.? I'm not saying that to argue your specific point. Neither of them is 'superior' to another. They both have different purposes, philosophies, and use cases. Each has its strengths, pros, and cons. And that is actually very cool.
> not to include type declarations because they can lead to a messy middle ground?
What? Type declarations in CL (which came from prior Lisp dialects) were added, so that optimizing Lisp compilers can use those to create fast machine code on typical CPUs (various CISC and RISC processors). Several optimizing compilers have been written, taking advantage of that feature. The compiler of SBCL would be an example. SBCL (and CMUCL before that) also uses type declarations as assertions. So, both the SBCL runtime and the SBCL compiler use type declarations.
I've only played with Clojure (not used it professionally, I'm working with Scala) but Clojure interop with Java is way better than what I can see here: https://abcl.org/doc/abcl-user.html The way it's integrated with the host platform makes it better for most use cases IMHO.
> The way it's integrated with the host platform makes it better for most use cases IMHO.
That may be. ABCL is running on the host system and can reuse it, but it aims to be a full implementation of Common Lisp, not a blend of a subset of Lisp plus the host runtime. For example one would expect the full Common Lisp numerics.
One of its purposes is to be able to run portable Common Lisp code on the JVM. Like Maxima or like bootstrapping the SBCL system.
There is a bit more about the interop in the repository and in the manual:
I didn't say "type declarations can lead to a messy middle ground in Common Lisp" - obviously they exist there for a reason, but, maybe they DON'T exist in Clojure, also for good reasons, no?
ABCL does exist, sure, and there's also LCL for Lua. Yet, 8 out of 10 developers today, for whatever reasons would probably use Fennel to write Lispy-code to target Lua and probably more devs would choose Clojure (not ABCL) to target JVM. That doesn't make either Fennel nor Clojure "far superior" than Common Lisp and vice-versa.
> I didn't say "type declarations can lead to a messy middle ground in Common Lisp" - obviously they exist there for a reason, but, maybe they DON'T exist in Clojure, also for good reasons, no?
Until recently (2023), the type inference was very weak and did not work with higher-order functions (map, filter, reduce, etc.).
As a result, Typed Clojure was practically unusable for most applications. That has changed as of last year. For instance, the type checker can now handle the following kinds of expressions.
(let [f (comp (fn [y] y)
(fn [x] x))]
(f 1))
This expression was a type error before early 2023, but now it is inferred as a value of type (Val 1).
Unfortunately, many Clojure users think types are somehow a bad thing and will usually repeat something from Rich Hickey's "Maybe Not" talk.
I've worked with Clojure professionally. The codebases I've seen work around dynamic types by aggressively spec'ing functions and enabling spec instrumentation in development builds. Of course, this instrumentation had to be disabled in production because spec validation has measurable overhead.
Although Typed Clojure has made remarkable progress, the most editor tooling I recall for Typed Clojure is an extension to CIDER that hasn't been maintained for several years. (The common excuse given in the Clojure community is that some software is "complete" thus doesn't need updates, but I have regularly found bugs in "complete" Clojure libraries, so I don't have much confidence here).
Overall, if one wants static typing, then Clojure will disappoint. I still use Clojure for small, personal-use tools. Having maintained large Clojure codebases, however, I no longer think the DX (and fearless refactoring) in languages like Rust and TypeScript is worth trading off.
I think the consensus is that it is not really mature enough for general adoption. Also, most people prefer to use one of the specification libraries that are available (spec, schema, malli). These allow you to do a sort of design-by-contract style of programming.