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.
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.
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.
Not that I “feel” it was no problem, but there were no bugs found that could be traces down to that.
It was not a small codebase.
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.
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.
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.
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.
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.
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
- Erlang's processes + supervision + fault tolerance
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.
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 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.
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.
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).
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??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
> And, technically speaking, yes, I agree, you do need to know all paths to be 100% certain about types.
Yes, thank you.
(declare (type Integer m))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.
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.
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.
> why then it can't be 'hosted' like Clojure?
ABCL does not exist?
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.
What were those reasons?
> ABCL does exist, sure,
Would that count as a hosted implementation?
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.
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: