It means you can avoid getting into the situation where half your logic is encoded in the type system and enforced at compile time, and the other half as values at run-time. It's all values at run-time.
And for the same reason, you don't fall into the trap of "puzzle-solving" with a type system. Between run-time values and the compiler there is a world of infinite possibilities that some type systems (and I would also include some macro systems!) seem to encourage adding more and more layers to. Clojure tries to speak the language of data, which is a lot more grounded.
We can develop our program while it's running.
And it's not a tricky thing that injects or restart anything, it's by design. Load file to REPL and now your functions are redefined. Just on this namespace. Load file/reload isn't a automagical thing. It's simple: just "stream" your file to the REPL. HTTP library, DB library, any library need to thing about "how do I hot reload", it's a language feature.
I see many developers arguing things like "types are important because avoid runtime errors". If you develop INSIDE runtime, you have no reason to fear runtime errors. Your runtime error will blow up during development process.
A cool thing about this pieces:
- I can start my app from REPL
- Connect my browser in this app
- Run my integration tests that create entities (inside the same REPL)
- See in the browser the state from app after run the test.
However, someone might turn around and say, “well, I can pull in a lib with immutable data structures in $lang if I want to.”
It needs to be highlighted that the real game changer is an /ecosystem of libraries/ built entirely out of immutable data structures.
That’s not something that can be engineered as needed.
Plus Clojure Core Teams laser focus on API stability makes 10 year old (and even unmaintained) libraries to just work. I have not come across a code snippet so far that did not work because of a breaking change, not saying they are not there, but I did not find it.
Yes, there are other Lisps, but Clojure also improved certain things compared to them:
- Clojure has way more reach. Being that it runs on the JVM, JS, CLR and has great interop. It means you can actually use Clojure instead of Java, JS and C# to do just about anything you could with those. That's not true of Common Lisp, Scheme, Racket, ELisp, where the main runtimes don't have a lot of money behind them, and where libraries trail behind.
- It disallows reader macros, so there's a limit to how wild people can customize it. This helps minimize the Lisp curse.
- It also has a pretty simple macro system, that is both hygienic, yet straightforward to use.
- It has a more visually pleasing syntax, by extending homoiconicity to also support maps, vectors, sets, regexes and keywords.
- It modernized some old remnants, like having first/rest instead of car/cdr.
- It ditched cons cells, and instead uses a proper sequence abstraction.
- It has proper true/false, and nil is not the same as the empty list.
On top of being a great modern and improved Lisp with bigger reach, it also is just a well designed language. Lets explore some of that:
- Functional programming as the default paradigm, but others are supported (logic, OOP, imperative) when it makes sense.
By default Clojure uses immutable persistent (fast and memory efficient) data-structures and variables are immutable. Functions support full closure over their environment. Anonymous functions are first class. Higher-order functions are included. Loops are handled recursively. The whole shebang. Yet, you can relax this in controlled way when it make sense. You can introduce controlled mutability, you can define polymorphic functions, you can create mutable types, etc.
- Every collection under the sun.
Data-structures are fundamental to computer-science, and Clojure has a bunch of them. Persistent Lists, Vectors, Maps, Sets, Queues. LinkedList, HashMap, ArrayMap, Array, DoublyLinkedList, HashSet, TreeSet, ArrayList, PriorityQueue, TreeMap, etc. All built on proper abstractions as well: Associative, Sequential, etc. And it has a ton of useful functions to operate over them as well.
- Awesome data manipulation constructs
Some people say information systems is all about taking information and transforming/moving it around. Boy does Clojure has you covered there. It has an awesome set of performant lazy data manipulation functions/abstractions called lazy sequences with things like: map, filter, remove, distinct, dedupe, group-by, sort-by, partition, split-at, replace, shuffle, reverse, rand-nth, etc. And, all of these are also available in an eager variant as well which performs loop fusion (called transducers).
- Great equality semantics
In Clojure, equal values are equal things. This is just awesome! Like, that's how a layman thinks of equality, and that's how programming languages should have it as well in my opinion. For performance, you can decide to use reference equality as well, but that's not the default.
- Full support for concurrency and multi-threading
Kind of self-explanatory, but Clojure has a lot of concurrency constructs which make it easy to write concurrent/multi-threaded programs.
- Types are open for extension and have good polymorphic support
A bit like traits and mix-ins, types can all be extended from outside their definitions, and polymorphism exists at many levels: dispatch based on the type of the first arg, dispatch based on the value or type of any arguments, dispatch based on the arity, dispatch based on some hierarchy, etc.
There's more obviously, but this is already pretty long. So I'll finish by answering your last question:
> Do you have any ideas what any other language (e.g. Python, TypeScript, Go, Rust, Julia, Haskell, OCaml) would need to change to make you as productive as Clojure?
Everything. I mean, they'd just need to become Clojure. The thing is, see how long my answer is? That's because it is not just one "killer feature" that makes Clojure awesome. Clojure has just the right balance of features all designed in just the right way to come together beautifully and coherently to create something that is greater than its parts. That said, you can have a look at Elixir, I think it gets closest to providing something that approximates Clojure.
Error messages could be improved, currently, they often leak the implementation details, so the error is disconnected from your actual code.
Startup time is slow. There's ways around, like making a Graal Native build, or using ClojureScript or babashka instead, but those are all alternative thing you need to consciously choose to use. It be great if standard Clojure somehow could start really fast.
Memory usage could probably be improved further. It makes liberal use of Objects right now for everything, and that adds up quickly.
Performance is pretty fast, but I'll never say no to something that would perform faster.
I think I would make the data manipulation functions eager by default, and the lazy one would be the opt-in one.
There's a few names that could be improved, contains? is a famous confusing one, since it always checks for key, would have been better to call it contains-key?
When it comes to the language design itself, I'm not sure there's much I'd change. I think it could be interesting to try and see if you could build some language that's like Clojure and have some level of static type safety. I'm not sure what you'd have to trade away for it, but I think it be an interesting experiment.
Oh, and one more thing, doing unboxed things, operating over primitives could be improved and made easier. This is often needed for high performance numerics, or making better use of memory and caches.