I've been working in Clojure now for about 12 years. Maybe 12+ years of Java prior to that.
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 gave Clojure a shot some years ago and even wrote an important tool used daily by all employees at our company in Clojure. The problem for me was maintenance. If I had to make any change to that code, I had to dive into the REPL just to understand it. Whereas with something like Java, even if something is poorly written and not documented, I can get at least a minimum understanding of the code just looking at the types.
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.
Haven't worked much in Clojure, but I have the same experience with Common Lisp. The malleability and live image workflow is just very pleasant to work with.
Clojure is like that, but because data structures are default immutable, and all of the standard library (and the vast majority of 3rd party libraries) are also default immutable, the stress level is much lower.
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.
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.
I’ve started learning clojure recently and I haven’t quite grasped the REPL yet. I really like it, but so far the type of feedback it provides feels similar (and sometimes inferior to) a static type system. I mostly use it as a tool for answering “wait what data does this return?” And of course trying functions as I’m building them.
Are there any tricks or habits you learn with the REPL that go beyond what a static type system gives you?
I worked with a 60K LOC thing* that talked to multiple services and had complex configuration. Ran fine on my laptop pointed at the company's dev env.
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
Forgot to add: multiple threads ... multiple DBs ... multiple topics on more than one kind of message queue ... hitting and serving REST endpoints -- all no problem for the REPL and the debugger[0].
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 use Integrant to make systems out of components that are tied together at runtime. I have the option of running real versions or mock versions in tests or the REPL, with config drawn from the environment or config files or parameters. I can boot or reboot any combination of components at will. It’s fairly easy to divide and conquer this way.
I like integrant, but I feel like a lot of the examples I see use it for each web endpoint, which seems like a massive amount of overkill to me. I tend to use it just for the router, but it sometimes feels like I'm not using the library to its full potential with that.
Even better than in small ones, because you don't need to spend 60 seconds building and restarting the application after changing one line: You simply redefine a single function at runtime and them can test the effect immediately.
I played with Clojure just a bit in 2014 because I wanted to write GUIs in Om, and this gave me a seriously warped habit of calling React.el('div',...) for a while. Sorry not sorry.
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?
REPL code is copy/pasted straight into tests. So really, REPL is for helping write tests.
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.
Neither defprotocol nor deftype introduce static typing into Clojure. Errors in their usage are not checked statically and are only discovered at runtime.
No, defprotocol and deftype have the same properties as Java interface and Java classes, and the types are checked at compile time. This is static typing. Period. Clojure is a compiled language, it does check typing during compilation.
Protocols do not work like Java interfaces or classes. Their methods are compiled into regular functions which lookup the implementation to use at runtime based on the runtime type of the receiver. Compilation will check for the named function but doesn't do any further checking. Given the following protocol and implementation:
(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.
deftype IS a Java class, it's not compiled into something else. What is a Clojure function? A Clojure function is a Java class. Clojure is a compiled language, so it does check types, just like Java check types.
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.
Of course Clojure has to ultimately be compiled into a native format for the host platform, bytecode in the case of the JVM implementation, but that doesn't require type checking in the same way Java does.
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.
But if you add type hint in the signature, it does check the type. Basically, if you specify the type, it will check type. Just like any language that is not automatically inferring types, e.g. Java. So it is the same as Java.
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.”
I've already shown that type hints do not constitute type checking:
(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.
If the core team had ever addressed the decade of surveys showing that error messages/stacktraces were people's top complaints, you wouldn't need Claude.
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.
> In fact, it can be nice to do one's explorations in the REPL and then reify one's discoveries as tests.
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!
Reify is a general term, it means to "make concrete" (or to "make real" depending on the usage) something that is previously fuzzy or abstract.
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.
I picked up Clojure recently after a 10 year hiatus. My early complaints were around tooling - a Lisp-specific IDE felt all but required to get the benefits of the REPL and structural editing. And leiningen, god how I despise that tool.
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.
One important detail is that in Lisps like Clojure, printed values can usually be read back as code. So, the REPL is really a read-eval-print-LOOP.
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.
It's totally different. The R in REPL retains its original reference to the Lisp READer which is special when your code is structured as an AST as in all Lisps. Only in Lisps can you select the left paren of a deeply nested function and hit Eval. Other language REPLs are really just shells by comparison.
> For most people Python/Javascript also does the job
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.
I think it's "around" because it's quite productive and sits on a rock-solid runtime with support for an astonishing amount of libraries.
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.
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.