Hacker News new | past | comments | ask | show | jobs | submit login
Six years of professional Clojure development (falkoriemenschneider.de)
174 points by yogthos on May 6, 2021 | hide | past | favorite | 200 comments



Clojure is the most enjoyable language I've ever used and I love the interactive development. I haven't written code in any other language that even comes close.

Unfortunately I am too lazy and careless to use Clojure in any serious capacity though. I really need a Haskell or Rust compiler to remind me of all my silly mistakes. I can't be trusted to get to the same level of confidence through unit tests or linting or spec or malli.

I actually tried using Clojure for a little web project (~3k LoC in both Haskell and Clojure) and any refactor that included maps that were passed around in many different routes (think authorization, logging, DB) ended up with me chasing down rather weird type errors. Basically the equivalent of can't call X on undefined in JS.

In Haskell I spent at least the same amount of time trying to make various monads play nicely together. Think using IO, Maybe, Either, etc. in the same route handler without having your code be indented ever further to the right.

But the difference is that once I've invested that time I'm very confident in my code, whereas in Clojure there's always this lingering sense of impending doom because surely there's some code path that I haven't thought about.


This looks like something Common Lisp does better, however I have too little Clojure experience to compare. CL (and SBCL in particular) does "good enough" static type checks, it throws warning at compile time (when we compile one function with a keystroke). We can also precise our function types gradually. It isn't a HM type system (Coalton[1] could be it) but it's already great (compared to no compile-time types at all).

Oh, about interactive development: that's sure, CL shines here. Objects get updated (lazily) after a class change, we can install Quicklisp libraries without restarting the image, etc. It's very smooth.

1: https://github.com/stylewarning/coalton


> Unfortunately I am too lazy and careless to use Clojure in any serious capacity though. I really need a Haskell or Rust compiler to remind me of all my silly mistakes. I can't be trusted to get to the same level of confidence through unit tests or linting or spec or malli.

I think most developers cannot be trusted to make these mistakes, I wouldn't call it "careless or lazy". Languages exist with tools baked into them to prevent slip ups. Why wouldn't you want to take advantage of that tooling?

Also, as a result of this comment section, this is the second time in the past week that I've seen the suggestion to use 3rd party typechecking/static analysis tooling in a dynamic language, which (to me) brings up the question: Do you really want a dynamic language, or do you just want the ability to be careless intermittently? There is no shame in saying "I just want to get happy-path code working quickly". There is joy in getting happy path working quickly. I just think, as a codebase in a dynamic language grows, codebases seem to build lots of tooling to make that dynamic language less dynamic.


> Do you really want a dynamic language, or do you just want the ability to be careless intermittently? [...] I just think, as a codebase in a dynamic language grows, codebases seem to build lots of tooling to make that dynamic language less dynamic.

I think this is getting closer to the interesting questions. It's often a good thing that when the product matures and moves more into maintenance mode, you can then incrementally add the kinds of rigidness that suits the needs of that. Clojure has excellent support for these in the schema style data validation tools of spec, malli, etc.

If you had a rigid system and slow turnaround from the start, you might not have gotten off the ground to get that far.


> If you had a rigid system and slow turnaround from the start, you might not have gotten off the ground to get that far.

I always see people say this and on the surface it seems to make sense, but I also always wonder what "rigid system" they're talking about. It's much, much faster to make big changes in every phase of a project in Haskell for me than it is in Elixir, so this is the perspective I'm seeing this from. This hypothetical/straw man "rigid system" is hard to fathom.


It's funny - I see that sentiment all the time ("Static typing is slower to get going with") and I kinda chuckle. In both static and dynamic languages, in the beginning, you're not doing anything silly. In both scenarios, you likely aren't writing your methods that accept strings and then immediately trying to pass them ints. For two developers of the same proficiency, competing to generate value in a static vs. dynamic language, I suspect the first 10 hours or so of building a project would progress pretty similarly. Compilers don't really fight with you until there's lots of hidden complexity.*

*Haskell and Rust get a lot of flack because of their compilers being difficult for beginners to work with. I don't necessarily think this is actually an issue - for a sufficiently skilled Rust or Haskell dev, I doubt most compiler errors feel like a "fight".


> for a sufficiently skilled Rust or Haskell dev, I doubt most compiler errors feel like a "fight".

That's certainly true of my experience. I prefer to have the compiler there guiding me (My reference points are poorly-typed languages like C, Java and C++, and the dynamically-typed language Python. I've never used Clojure.).


I don't have Elixir experience, but the natural way to get started in modeling data and state in Clojure quite obviously involves less ceremony and scaffolding than what you do in Haskell, and is less work to change around.

Also, it's not just the amount of work, it's the complexity of the language. Clojure is really simple, so you don't h ave to spend much of your cognitive capacity juggling things related to the language or operating a mental simulation of the type system.


I totally understand what you mean! At least I think I do. I have spent a significant amount of time going down the "type flex" rabbit hole. You can always make thinks a little safer, a little more concise, and so on.

In that sense I really do get off the ground much, much faster in Clojure. In fact, it's my favorite language for prototyping and the whole interactive execution model of the REPL makes we want to learn Emacs and do everything in some kind of lisp, rather than awk, sed, bash, dash, whatever.

I think the key ingredient for me with Haskell is to know when to stop. You can be very productive even as an intermediate Haskell user if you restrict yourself to the features with the highest return on investment. Everything else can be done later. But I also know that it's very tempting to stress out over these details.

As a very late conclusion to my various comments in this thread: if I got better at taming type related errors in Clojure I could very well see it being my go-to language for everything.


Yeah, it seems like programming language decision is, at least in part, a proxy for "At what point do you want to be held accountable for your program's correctness at runtime?"

Haskell/Rust - Immediately

Java/C#/Go - Relatively quickly

Dynamic Languages - Whenever you decide it becomes important


It's a bit of an oversimplification, I think.

Even though Clojure is not a statically typed language, it does have a type system.

Here's an example what Clojure.Spec lets you do. When I was working for a fintech company, we built a suite of specs for a ledger. Based on those specs we could generate data. And it wasn't just some set of key/value pairs with completely randomized numbers. It would generate "a proper" ledger, where every number in a transaction depends on other transactions.

We used that generated data to render UI locally and on the non-prod environments.

Using the same specs we build data validators, we re-used the specs to validate data in the input fields in the UI. Which is totally bonkers. How the heck you achieve code re-use between completely incompatible ecosystems - in our case, JVM and Javascript? Even Nodejs doesn't always let you re-use code between backend and the front. Clojure allows you.

Using the same specs, we've built property based/generative tests.

I've never experienced the joy of creating such robust, predictable, and reliable software with any other (statically typed or otherwise) language before.

I'm not saying you cannot build a similar thing (or even better) with Scala or Haskell (or some other PL). The simplicity of how Clojure allows you to write things like that - just incomparable.


I'm not familiar with Clojure Specs, but it definitely sounds exactly like the "Dynamic language implements type checking tooling to assert correctness", as referenced in the above post. The spec website just looks like type assertions as "validation" - what am I missing?

> Even Nodejs doesn't always let you re-use code between backend and the front.

This is not true. Check out Lodash. It's a dependency of pretty much every FE and BE NPM package that exists. Ignoring rendering libraries like React, client side JS and backend JS are "just" JS.

> I've never experienced the joy of creating such robust, predictable, and reliable software with any other (statically typed or otherwise) language before.

I think you should definitely keep doing the things that bring you joy!


> This is not true. Check out Lodash.

Respectfully, as someone who spent over a decade building front-end apps, I'd say: the code re-use between js in the browser and js in node still feels limited in comparison with Clojure/Clojurescript approach.

I'm not talking about utility libraries.


Because you chose to bake environment information into your code, that does not mean nodejs doesn’t support client/server reuse. Respectfully.


I never said "it doesn't support", it's just not the same.

Code re-use is just one of the aspects. When choosing a tool, especially a tool like a programming language, one needs to take "a holistic approach". Clojure is not a silver bullet. Neither is Haskell, Rust, or any other PL.

I like Clojure today, because today it makes sense for me to use it. For the projects I build. I'm sure, someday, it stops making sense for me to use it, and I will move onto something else.

You can use all sorts of arguments for people to try something else today, assuming they are just misinformed - either about Clojure or the other PLs (you're preaching about).

I can assure you though, most Clojuristas I know - didn't end up using Clojure by accident. It's a deliberate choice. After trying many other options.


Whether you get strong assurance of correctness from static typing depends on your domain, are you're working with a closed-world app like a compiler, or a talking to the rest of the world a lot?

Though it's telling how little static checking by type systems is leveraged eg in LLVM, so even in closed systems the assurance payoff for static checking doesn't necessarily get good bang for the buck.

Eg working with web based services, your code tends to communicate a lot with other services, and a very large % of bugs that make it further out than your dev laptop come from those interfaces. Making these part of the closed world of the static type system is done in some places but most static language users elect not to do it, preferring programmable checks in form of schemas or tests. Which I think tells something.

I tend to think of static vs dynamic as a mindset question, where our bias of loss aversion rears its head. It's an attractive idea to hedge your risks by usign a static language even if it only catches the trivial bugs and slows you down, because statically found bugs are a falsifiable observation, wheras making programming easier, faster and more fun[1] in general doesn't leave hard evidence unless you run duplicate trial projects just for science.

[1] For some personalities, you might derive more fun and satisfaction in the mathematical certainty of static languages. Or you might be in a work environment where you don't have room to find fun anyway. Or maybe you don't even like programming. Yet others find fun indispensable: fun is necessary for keeping up morale.


> Eg working with web based services, your code tends to communicate a lot with other services, and a very large % of bugs that make it further out than your dev laptop come from those interfaces. Making these part of the closed world of the static type system is done in some places but most static language users elect not to do it, preferring programmable checks in form of schemas or tests.

Are you actually suggesting that "most static language users elect" to not decode at the boundaries of their system because it somehow is favorable to not do so? This does not match my perception of things, neither the "most are not decoding" part nor the "it's fine to not decode into a known good structure" part.

> It's an attractive idea to hedge your risks by usign a static language even if it only catches the trivial bugs and slows you down, because statically found bugs are a falsifiable observation, wheras making programming easier, faster and more fun[1] in general doesn't leave hard evidence unless you run duplicate trial projects just for science.

What an incredible straw man you've built up, but then you just let it sit there and don't even burn it up.

Since I'm not here to convince anyone, I can say your straw man is the opposite of my experience. I've never been able to prototype and change a system in any phase of its lifetime faster than I've been in Haskell. Haskell has downsides, but speed of development and maintenance is one of its strong points.

> I tend to think of static vs dynamic as a mindset question, where our bias of loss aversion rears its head.


> Since I'm not here to convince anyone, I can say your straw man is the opposite of my experience. I've never been able to prototype and change a system in any phase of its lifetime faster than I've been in Haskell. Haskell has downsides, but speed of development and maintenance is one of its strong points.

Yup, and when it comes to fun, I've never had more fun programming that in Haskell, where I let the compiler check the annoying fiddly bits so my brain is free to tackle the meaty problems!


> Are you actually suggesting that "most static language users elect" to not decode at the boundaries of their system because it somehow is favorable to not do so

I'm was thinking protocol meta systems like protobufs where you use tooling to enforce closed world style communication patterns in a networked world.


I don't particularly want to argue with you. Using the phrase "falsifiable observation", and then referencing "fun" as an argument just doesn't really work for me. I appreciate that you like dynamic languages, but I will not take your "rest of the world" bait. To suggest that dynamic typesystems are as verifiably correct as static typesystems is verifiably incorrect.


Not sure if this needs clarifying or not from your message but:

By falsifiable I meant that it's an observation that could be shown to be false, if it was false. So a concept from philosophy of science, not an assertion.


I've been using Clojure since the summer of 2009, started a startup with it, using it exclusively on one project right now... and I agree about static types. I love many many things about Clojure, its my favourite language to use and I find it very well designed over all. But proper first class static types are the one thing I wish it had.

My dream programming language is basically a statically typed Clojure.

I've on and off poked at trying to make something like that (parsed using instaparse, type checked in Clojure, compiled to C++ using https://github.com/arximboldi/immer for the data structures), but haven't had the time to really get anywhere with it. Plus, even if I succeeded, I wouldn't have the rich Clojure (and by extension, Java and Javascript) ecosystem.


Do you know about the Typed Clojure project? More or less Racket's contract system, for Clojure:

https://github.com/clojure/core.typed

To me, it's one of the great testaments to the power of Lisp that you can bolt on a static type system after the fact.


Yes, but it’s really not the same as first class static typing support. It also doesn’t help when none of the libraries you use have type annotations and when I last looked it was also quite slow.


I wrote Clojure for 4 years. I understand your pain points. Dynamic types are an impediment beyond a certain code size.


My team structures projects by breaking things up into small isolated components that can be reasoned about independently.

We'll often do it at the level of namespaces, where a namespace will describe a particular workflow or data transformation, and namespaces tend to be 500 lines or less. It's a similar idea to microservice architecture without the overhead of having to actually split the application up into separate processes.

We'll also often pull out code into libraries when we notice it being generally useful outside the original use case.

I find that there are a lot of benefits to structuring your code using small components instead of monolithically as tt's easier to reason about and reuse. Any large application can, and in my opinion, should be broken down into small parts that are then composed.


``` breaking things up into small isolated components that can be reasoned about independently ``` - This is insufficient for the same reasons unit tests are not enough and you also need integration tests. The moment you cross namespace boundaries, you will end up not setting keys /entries in maps , missing logic etc and end up needing something like Spec/Schema....


Sure, and my team uses Spec at component boundaries to define APIs for that reason. I actually prefer this approach to static typing because it makes it possible to add a specification at the level where it provides the most value, and it's much easier to express semantic constraints using this approach.

It does take more discipline to work with a dynamic language, but once you develop a process then it can be very effective. It's also worth noting that immutable by default plays a huge role here. With Clojure it's natural to create truly isolated components while it takes a lot more discipline in an imperative language where things are passed by reference creating implicit coupling.


> you will end up not setting keys /entries in maps

This is what I run into. But that's not exactly a dynamic language problem. It's more of a data-oriented programming (in the Clojure sense) problem.

I'm coming to the opinion that data-oriented programming techniques only make sense inside a fairly tightly bounded context. One that's small enough that you can see and understand the whole thing at once. As soon as you've got ad-hoc data structures crossing logical boundaries, you lose the ability to keep track of it all, and it becomes very difficult to ensure everyone's interacting with these ad-hoc types in a compatible manner.

Incidentally, this is also exactly why I dislike JSON for APIs. I'd much rather share data across boundaries using data structures with explicit, nominal, static types. Like what you get in gRPC.

Anyway, my Clojure experience is limited, but I think this is why I have an easier time letting Python code get big than I did Clojure. With Python, I've got myself into some problems with data oriented programming, too. But with Python, it was easy (and idiomatic) to walk that back and switch to using dataclasses.


> This is what I run into. But that's not exactly a dynamic language problem. It's more of a data-oriented programming (in the Clojure sense) problem.

Yes, all dynamic languages have this problem. I have transitioned to statically typed languages and it has been very peaceful so far :)


To go at it from another angle: I'm working in a relatively large Java codebase that also likes to pass around generic ad-hoc data-structures such as maps. And I'm having exactly the same problem problems there. Static typing does nothing to help the situation.

I realize that data-oriented programming is more likely to happen in dynamic languages. But correlation is not causation.


IMHO Static typing => structure, not necessarily Map<String, String> (which is also a type). Shaping data is quite important and having an associative array doesn't necessarily means that the data is shaped.


Right. . . but, way up at the start of this, you specifically called out using associative arrays for all your data. And what I'm saying is that, while that's absolutely true, it's also technically an orthogonal question from whether you're working in a dynamic or static language.

Yes, sure, we can say that no true Scotsman would use a static language that way. But I think that's maybe missing the point. If you can dismiss this practice in static languages by just saying, "Well, maybe you shouldn't do that," why not also apply the same dismissal to dynamic languages?

That said, there's a specific subset of dynamic languages that make it more difficult to do your domain modeling any other way. Clojure and JavaScript belong in this group. I'm not sure of other examples. Basically, if the type system has been set up such that everything is either a map or a glorified map, you aren't left with a whole lot of other options. But not every dynamic language does that.

In Python, for example, I always use dataclasses for data. As long as I'm being good about this, I don't actually need to explicitly write unit tests to verify that types are being handled properly. MyPy's got my back. And, even if I'm not using it, unit tests for covering actual behavior will typically blow up with a type error in about the same amount of time that it takes a static language's compiler to detect one.


Humans are weak. The easier it is for them to do the wrong thing, they'll do it.


> We'll often do it at the level of namespaces, where a namespace will describe a particular workflow or data transformation, and namespaces tend to be 500 lines or less

Are there any open source Clojure projects that are structured this way? I'd like to see a working example.


My experience with dynamic types comes from developing in Python. The projects I've seen were pretty healthy until a large enough part of the original team went away. Once enough people had to figure out their way by experimenting, the projects got messy.

I'm not saying this doesn't happen in Java or Go but the static typing enables easier code exploration. That makes code bases more readable to newcomers.


I always really missed header files when working on Python.

A sort of enforced distinction between (typed) interface and implementation.

In practice I always wound up needing to read code of functions to see what types (or keyword argument!) they expected.


That might be true but good variable names and some terse but to the point comments are much more helpful in my experience.


- Clojure without clj-kondo

- PHP without phpstan/psalm/phan

All bad ideas, modern tooling can detect errors in your code before you finish your code expression, but you do need to go out of your way to setup it up

Most people do not and then complain about types instead of talking about static analysis

If you are ever coding Clojure you should be following this: https://github.com/clj-kondo/clj-kondo/blob/master/doc/edito...


> then complain about types instead of talking about static analysis

Static types are a form of static analysis. One that is deeply integrated into the language and compiler, which gives obvious advantages such as machine-checked documentation, custom invariants and error messages (expected a Currency but got a Country), better code generation, better tooling (IDE pop-ups, fast incremental checking) etc. Other forms of static analysis are of course useful too.


It seems like if a tool is so essential to successful development in a language, it should probably not be a 3rd party library but instead baked into the language tooling itself, no? (I have never used Clojure and am not trying to be inflammatory)


I use kondo already, and while it's a nice tool it doesn't solve my problems.


Can you be more specific? There is a primitive type annotation checking system in there, kinds of mistakes not being caught will likely be of interest to Borkdude and others


The features of the Rust and Haskell type systems that are most useful to me are requiring me to handle every possible null value and modeling branching control flow with algebraic data types, including exhaustiveness checks for those.

A value of type "User" can be either "LoggedInUser", "Admin" or "NotLoggedIn", and each of those can have data attached to it. Now I can write functions that only work on specific versions of this and the compiler will remind me to handle every possible variation. If I've narrowed it down to "this function only takes Admin", then the compiler will prevent me from passing the wrong type to it.

It's little things like being able to wrap a "String" in a so-called "newtype": "newtype SpecialString = SpecialString String". Now a function can happily accept three string arguments, but if each one is wrapped in a special type you can't mix them up. Or saying something is a "NonEmptyList" and now I can rest assured that it'll always have values.

If any of these can be achieved reliably (<-- important!) in Clojure then I apologize for my lack of knowledge!


Yes, just use C with asan, msan, valgrind and frama-c. Why invest in types?


Check out Julia; It has built-in optional static typing, a fast JIT, it can be run interactively in the same style as Clojure, and it supports lisp-like macros.


I use & love Julia, but it doesn't have really static typing – it has runtime validation based on type annotations. For static typing to be useful, IMO, you need static typechecking, usually done at compile-time.


This is a common point of confusion for people. Julia has a useful and expressive type system, so people just assume it must be static but it's not, julia is dynamically typed.

It's intermediate representation is statically typed, but the language itself has dynamic semantics.

That said, we have almost every trick and tool up our sleeves that static languages have (in various stages of development). Check out https://github.com/aviatesk/JET.jl if you want static typechecking of julia code.


Is there something for clojure that TypeScript is for JavaScript?


There are a few such things.

The most comparison is core.typed, which literally just adds static (gradual) typing to Clojure. That said, I think that's a misleading comparison, and less useful than it is for TypeScript.

If you're willing to relax the definition to "static analyzers that find similar issues to what TypeScript might find", there are a few that require very little input (but are less powerful in finding these types of bugs, more powerful in finding others) like e.g. clj-kondo or spectrum.

One fundamental challenge is that the idea that you should mostly just pass data around, ideally maps, and those maps should be open for extension, is very core to Clojure. You get a lot of benefits from that, but the idea that anyone can add stuff and that's OK also makes it difficult to detect that some accesses are _not_ OK.


Clojure has an optional type system: https://github.com/clojure/core.typed

(note that Lisp is flexible enough that a type system can be written as a library)

There's also this for run-time checking (i.e., you won't get any help at compile-time, but functions called with bad values will fail in very clear ways): https://github.com/plumatic/schema


Hopeful new lispers beware that core.typed is not so easy to use as TypeScript, and slows things down quite a bit, and you will need to write a lot of type definitions for 3rd party code. Most (all I’ve seen) clojure shops instead rely on runtime type checking with Spec or Schema, both still popular


What helps me with this inherent uncertainty is writing in a functional style (simple inputs and outputs, minimal functions) and using runtime tests to check for correctness. The key is to have the tests as far down the abstraction tree as possible. There's a performance hit, but you can use macros to keep it out of production.


I also like the racket contract system, it is very expressible. But comes with a performance penalty that i can't justify for anything inside a loop. Typed-racket might help but i haven't used much so not so sure.


Haskell or Rust is too far the spectrum ;), TS or Kotlin are a nice sweet spot.


Why is everyone making a big deal out of dynamic nature of Clojure as the main factor to not consider it in enterprise project ?

As if no dynamic language has ever succeeded in creating enterprise application which runs 24X7 !!!

If you don't like maps use clojure records

If you don't want free floating initiation or ambiguous variables and arguments to function in code, use type hints. Type hints may not be exact alternative but are good enough help in reading the code and especially compiling the code to native graal image or just providing enough information to JIT for optimization.

Use spec for generative testing and more detailed documentation. Irrespective of the language you use, tests are much important than types. Types help but they are not commandments.

Multithreading capabilities in clojure through standard library primitives exposed to achieve it are much better and safer than java.

For most of us, cost of immutable data structure is pretty reasonable. But if you think it's not the case, clojure provides mutable alternatives.

Use components like library to large code in depedancy inversion oriented style, if thats what you like. Personally I prefered dynamic dispatch through multimethods when I used to work in clojure 4 years back.

You can always get the library to do your job in the clojure ecosystem. If you don't find it, clojure provides easy integration with java libraries and you might be able to build a wrapper around it in couple of days if it is complex, in minutes otherwise.

To compare Clojure with languages like Haskell and its features, Rust and its features is utterly naive. Instead development team must strive for what actually is needed for job. Flexibility and discipline provided by language compiler and library eco system will always strike some kind of trade-off. Clojure is good trade-off for most applications including mission critical soft realtime application category.


The soft real-time interests me. Are there ways to avoid garbage as with java to minimize GC driven latency.


It's very much possible, now more than ever. New JDKs provide more efficient version of garbage collector like epsilon for example. CMS can also do the job if you know few tricks. You don't have to be a big nerd to understand them. Documentation is pretty helpful.

In my application I was able to get latency for http/rest services less than 15ms which also hit database more than once. Ofcourse it's very subjective to the business use case and networking infrastructure. YMMV

If you really want me to pin point the issue, as far as GC on JVM heap is concerned only major difference with other language implementation was that I had to use around 1GB of extra RAM.

All i am saying is that, start with idiomatic clojure. If that's not performant enough for you use case, start optimizing the code.


I assume you're referring to my comment, since I mentioned both Haskell and Rust.

_I_ am not capable of writing a robust, large scale application in a dynamic language. I do not have the discipline to second guess everything I do. I will at some point forget to write certain tests. Or test only the happy path. Or make a silly typo. Maybe I'll rename a keyword and in one place not notice that I'm renaming a keyword that will come from a completely different map. Or I'll forgot to namespace keywords and wonder where they come from.

I have no doubt that there are people who can write rock solid stuff in C, or Bash or Dash.

I can't. Therefore, yes, its dynamic nature is what leads me to rule out Clojure. I want to be lazy and focus on the logic, the algorithms and data structures. I do not want to be my own compiler because I'm not good at that.


I loved clojure until working with it for a few years, with some of the famous best teams in Europe and America, including Cognitect people, who ill leave unnamed here.

I fell out of love when I realised what the language is - a mutable, imperative, blocking IO by default language with lambdas and zero guarantees at compile time. Effects happen at any time, as it is an imperative language. Just like JS with a better syntax but with blocking IO and Java threads.

Backends, when more than a handful coders program, turn to runtime checking all the time with spec or schema, and because it’s always up to you to make things async, things often end up depending on a handful of blocking calls. It’s not impossible to write good big programs in clojure, but when working with a group it does not tend to end up that way. Changing a program from sync to async with zero help from the compiler is a task that ends up requiring a lot of time, and negotiation, when it could’ve been the default.

In imperative languages, where you have no control over what effects run, programmers often turn to frameworks to square their program. In teams of >20ppl using clojure, it instead tends to become siloed, where a few people “own” a part of the code, and others must request changes there. It’s not a law of nature, but a pattern I’ve seen emerging in my experience working with clojure teams, including the very famous ones. Time spent negotiating and in these meetings is time wasted.

I should make sure to thank Clojure for getting me into lisp and emacs though, as well as Hickeys enlightening talks.


What is mutable by default in Clojure? Nothing. You need to explicitly declare mutable variables with a special notation. Are you talking about the underlying VM? Yes, the Java API are mutable, not surprise here, it's not Haskell.

Imperative? What is imperative by default in Clojure? Nothing. It's one of the least imperative Lisp, favoring functional constructs all over the place unlike Common Lisp or even Scheme.

Blocking IO. Ok. So you want JavaScript? You have async i/o lib for Clojure and the primitive of the language to create threads are relatively good (relative because the limitations come from the Java VM).

Zero guarantees at compile time. Almost true. It' s a dynamic language and does not pretend to be something else. It has pros and cons. Like modelling a domain is very simple since you almost always reuse the same datastructure and having ubiquious immutable datastructures build a solid foundation for an ecosystem of libraries.


> What is mutable by default in Clojure? Nothing.

All data in Clojure inherits from java.lang.Object. I get what you’re saying, but in real life commercial projects, especially when you use libraries and Java classes, the code ends up with a lot of mutable things. You can never know if any function you call does mutate something without keeping all definitions in your head, and with 20+ people on the project, this is a challenge. Programmers of languages less sexy use frameworks to manage state and effects, but not in FP, which clojurians think they do.

> Imperative? What is imperative by default in Clojure? Nothing.

Please note that I’m not praising Common Lisp or Racket either - the same criticisms apply to them, except they ship with type systems. In Clojure IO is usually done by calling a function that does some (blocking) effect and returns something. That is programming in imperative style. In practice effectful functions find their way into programs, and taints everything referring to it. For this reason, clojure programs written by teams tend not to be as FP.

> Blocking IO. Ok. So you want JavaScript?

I for sure prefer to write a backend where no teammates ever introduce blocking calls. I once worked on a webpage which took minutes to render, because of blocking calls on the back end. It was a mess, after having been worked on for years by around 70 coders, including consultants for the biggest names in Clojurespace.

> Zero guarantees at compile time. Almost true.

Except for syntax errors.

These things turned me off: imperative and blocking IO and effects and no square framework to contain it, combined with zero compile time checks. It results in teams making bespoke solutions that other teams in the company (or even consultants from the famous players in the Clojure space) solving their code needs by meeting with the authors and asking for changes, having a ton of testing to make sure it does not fail in rare code paths, and having one guy on every team religiously adding specs in order to not go mad.


> I for sure prefer to write a backend where no teammates ever introduce blocking calls.

Please note that forcing an execution model in the language will inadvertently cause headaches for some kinds of domains. Since clojure makes no wild choices the language can be ported to other runtimes (.net, js) and benefit from future runtime improvements without breaking the language (JVM virtual threads, value types).

Since the core is small and lisps are extensible, we can have a multitude of choices in libraries. So if you really like typy things you can check out core.typed, spec, schema, malli etc. If you want async you can check out manifold, promesa, core.async etc. If you want non-blocking effects handling you can check out missionary or darkleaf/effect.


Language design is about what features not to include. If you include blocking IO, or allow unsound programs, disallow GC-less allocations, you are forcing a model of programming onto the user, which is a good thing. Node.js became a hit because it lacked the feature of blocking IO.

Having many choices available, and teams of maybe 50-100 good mates coming and going, and a period of 5-7 years, has a high likelihood of producing chaos code.


I agree, constraints often bring benefits. The question for a language is which ones to bring in and which ones to leave open. From that perspective I value clojure's choices (and don't value js every-io-is-a-callback design).

Any project of the size you mention needs technical leadership and constrained choices (for IO we use this, we do http this way..). This will be true regardless of what language you're using.


Inheritance from java.lang.Object does not make Clojure data mutable. Blocking for IO does not make Clojure imperative, it not a purely functional language, but its still a functional, not imperative, language.

It's possible to build a service in a purely functional language using non blocking IO that also takes minutes to render. No language will save you from a bad design.

I do agree that Clojure is not the most suitable language for large projects with lots of contributers because of it's dynamic nature.


The problem is not the built-in Clojure data structures. The problem is that a huge part of Clojure's heavily marketed value proposition is seamless-ish Java interop, which means working with Java objects and all their associated mutability. So while pure Clojure has very controlled mutability, pragmatic Clojure inherits all of Java's problems. And since they hype interop so much, nobody wants to rewrite functionality in Clojure when they could just leverage Java.


In practice all the interop lives at the edges of the application. As a concrete example, Pedestal HTTP server has around 18,000 lines of code, and 96% of it is pure functions. All the IO and side effects are encapsulated in the remaining 4% of the code. This has been a common scenario for the vast majority of Clojure programs I've worked on in the past decade.

https://www.youtube.com/watch?v=0if71HOyVjY

Core of the application does data transformations using native Clojure data structures. This is where all your interesting business logic lives. The interop lives at the edges and typically used for stuff like database drivers, HTTP servers, file access, and so on.


Thank you for these comments they are super interesting to me as a relatively new Clojure programmer.

Can I ask two related questions -

1. Reading between the lines it sounds like the mutability issue arises from interop, both in the core code and any dependencies that wrap Java?

2. If yes, curious why did the folks involved did not write native Clojure libraries to eliminate the (worst?) blocking dependencies (or patch the Clojure wrapper libs)?

I ask 2 because my understanding is interop was partly a bootstrap strategy for the language and there was always (I think?) an assumption that the native Clojure library ecosystem would grow and make interop less necessary. Is that not happening enough?


The whole point of running inside another ecosystem is to reuse existing solutions. Many wrappers hide mutability and provide an immutable API. And more java libraries are being written with immutable classes.

I also failed to understand the "everything is a java.lang.Object" sentence. It doesn't make a clojure map mutable. In the end it's all just ones and zeroes, but that misses the point.


>The whole point of running inside another ecosystem is to reuse existing solutions

Ya totally, but I think OP was hinting at a fundamental tension between this benefit of interop and the desire to be functional where Java is (generally) not. Clojure lets you reuse the wonderfully huge library of Java solutions -- but those solutions are (generally) not written with immutability in mind, which I could see becoming an issue when you're trying to write a concurrent app. I suspect that's what the java.lang.Object reference was about, although obviously I'm not sure.

>Many wrappers hide mutability and provide an immutable API. And more java libraries are being written with immutable classes.

All that sounds encouraging....


I really like Clojure and I'm glad it is around, but I'll prefer something with strong types any day.

From the article I see a lot f reasons why:

> "Don't break things!" is part of the culture.

If you cannot have compile time guarantees on correctness, the discipline to not break things becomes a key feature. On the other hand, if you have strong correctness guarantees, you may more easily incur some breakage (and thus actually fix things).

> Some discipline required [...] with great freedom comes great responsibility.

There's always some discipline required, but without a type system discipline become more important.

> you often need to find specific places where a function or a piece of data is referenced [which is hard in Clojure] because there are no links established via a type system.

Yups. IDEs also like types to build their features on.

> Documentation becomes more important

I dont like documentation as a fix. Docs should tell me why some code is there, not what it does or how to use it (that is preferably obvious).

Again: I like the language a lot. I also like Ruby a lot. But when a new project with serious complexity requires me to pick language I go with something that has: no implicit nulls, strong type safety, sum types, exhaustivity checks.


If you constantly work on the same code base day-in, day-out, breakage through the language or libraries is much less of an issue. You can keep up with current developments in the language and/or libraries, and update as necessary. Incremental changes are not a big deal, since you’re always around when they happen.

If you tend to write code once, and come back to it months or years later, breakage is a huge issue - why is the code that was working perfectly well in 2019 broken today? Why do I have to now fix 4 dependencies in order to do what I actually wanted to do (make some small change). This is the reason that I tend to write all my own code in TypeScript projects, but for my Clojure/ClojureScript projects I am much more happy to pull in the occasional library.

I would say that the “don’t break things” culture is less about static vs dynamic types, and more about the style of software being written and the maintenance expectations.


Dont break things merely exaggerated as a requirement in dynamic typed langs.


In real life projects, you end up doing type checking at runtime, meaning you also need a test for the code path


Exactly! More developer discipline required.

What ever can be checked by the compiler, as part of the language, is automatic free productivity to me.


The counter argument is that in clojure you can do (merge {:a 1} {:b “yo”}) without defining new types.


> Again: I like the language a lot. I also like Ruby a lot. But when a new project with serious complexity requires me to pick language I go with something that has: no implicit nulls, strong type safety, sum types, exhaustivity checks.

Which language is that?


Haskell, OCaml, Elm


> If you cannot have compile time guarantees on correctness, the discipline to not break things becomes a key feature. On the other hand, if you have strong correctness guarantees, you may more easily incur some breakage (and thus actually fix things).

Yes, and the result of that is cabal hell. Types aren't there to help you break your API. Once the API is out and it has users it is rude to break them. The linux kernel is a prime example how far can you get if you don't break your users.


Cabal hell is fixed with tooling. Part of that tooling is running tests, but a large part of it is "it compiled together so it should work together" (which is a safety dynamic typed langs dont offer).


No it is not. People still reach out from stack when a dependency has updates and it's not in stack yet.

Nobody cares if it compiles together. The point is if a new feature or a bug fix is introduced in a breaking way you cannot bump your dependency and start using it, or progressively move to the new API.


In my experience Stack solved all my cabal hell. I've come to accept some level of dependency juggling, as I had it with every languages.

In Haskell is was more laborious before Stack (the place known as cabal hell) and it got better than average after Stack. Sure, this is just my subjective experience.

> Nobody cares if it compiles together.

Well I didi. If Stack guarantees that a set of software compiles/tests together, then I dont have to do that work.


I don't completely agree with all of the points but I am really glad that there are sensible critics of Clojure who don't position it as a perfect language.

I have been working exclusively with Clojure for over 2 years now and I came from mostly C# background and I still want to pull my hair out every time I have to fix a bug which wouldn't have happened in a language with a good compiler. Same goes for asynchronous programming, which I think is generally lagging behind by many years in JVM world.

Another issue I have is the lack of choice in quality third party libraries. A lot of them are created as wrappers over Java libraries and then abandoned and those that aren't usually lack in documentation and depth.


So what is your preferred alternative/language/framework?


TypeScript+immutable is like typed clojure without S-expressions but with async io, so that is nice. Haven’t found a way to get eval-expr-at-point for js though. This is my choice for writing back ends today, but I also do Clojure jobs.

I program compilers nowadays, and do so in OCaml, which is blocking IO, mutable data, and no sexpr, but the type system makes changing code easy. For the domain of compilers, there isn’t much IO anyways.


> *Reusability*: Here's where Clojure really shines. Because most functions are pure and operate on few common data structures (mostly maps, vectors and sets) it is very easy to write reusable code. We created many internal libraries just by separating the candidate functions into their own namespaces within the regular project source tree. To finally establish the library we move the code from the project repo into a new Git repo and include the resulting library Jar in the project.clj as dependency. Thus, we have a very lean process that results in production-quality resusable assets.

This is very true. Data-orientation really helps with reuse and library composibility.

The ability to reference git repos in the deps.edn file further supports this process. I don't particularly like the ergonomics of the Clojure CLI, though, but having this feature is pretty great. I use it all the time.


I also had the privilege to work with clojure for a couple of years on a large project. That was wonderful. Hands down the best language/framework/approach to development I’ve ever had. I still find crazy that it’s not one of the main programming language out there.


Unfortunately, despite consistently hearing about how enjoyable Clojure is, I think many people are turned off by the JVM. This is probably also the problem with Scala, which is apparently also enjoyable to use.


JVM and .NET are the top runtimes with best in game GC implementatios, JIT compilers, JIT code cache, and monitoring tools for production code.

In fact plenty of them, given that there are multiple implementation to choose from.

Meanwhile other languages keep playing catchup and having to rewrite extensions in C.


The runtimes themselves may be great, but the tooling is bloated and uncomfortable.

Every time I use Clojure, I am reminded how much I hate using Java. The error messages alone are enough to turn me away. So is having to set up a whole Leiningen project just to get started.

A Clojure without Java would be very attractive to me.


For writing simple scripts and stuff I like to use babashka[1], which is a simple clojure interpreter that's compile to a binary, so there's no JVM or leiningen to worry about. Of course, it's slower than the JVM hosted clojure once the JVM is started, so it's basically best for simple tasks.

[1] https://babashka.org/


I usually use Cider + Emacs for Clojure. Cider let’s you interactively check values and makes error messages more understandable, at least to me.

I think that many Clojure programmers skip the JVM and use ClojureScript with node. Personally, given my history with Java I find that Lisp + JVM ecosystem is why I use Clojure sometimes instead of Common Lisp.


There is nothing else that gets close to the world promised to us by Xerox PARC.

Don't blame Java for the issues that are Clojure's fault.

The only Lisps that can match such tooling are Allegro and LispWorks.


Well, I owned a Xerox Lisp Machine from 1982 to about 1987. True, a great programming environment, but the situation is so much better today. For example, I own a LispWorks Professional license, and the support and quality of the product is fantastic. My friends at Franz have similar quality products like Allegro and AllegroGraph. Even Clojure+Cider or Haskell+Intero or Haskell+VSCode (easiest to set up) is arguably better than dealing with lack of deployment options for Lisp Machines.


Yeah, let me take the opporunity to thank you for the books.


you are welcome!


Your comment prompted me to google for those tools once more. One that I did not know about before is Eclipse Memory Analyzer. The screenshots[1] look promising, especially "Path to GC Roots". I didn't know that was possible. Is there something comparable for .NET?

[1]: <https://www.eclipse.org/mat/about/screenshots.php>

EDIT: A bit more googling and I found <https://memprofiler.com/> for .NET which seems to have a similar feature, called "Instance Graph".


Eclipse Memory Analyzer is quite good, but something like Java Flight Recorder is much more capable.

On the .NET side of the fence, you can have a look into this nice article although a bit old,

https://mattwarren.org/2018/08/21/Monitoring-and-Observabili...

and "ETW - Monitor Anything, Anytime, Anywhere"

https://www.youtube.com/watch?v=ZNdpLM4uIpw


as both a Scala and Clojure polyglot I would pick Clojure over Scala every single time, the Clojure REPL development style alone puts Scala behind.

Scala and SBT is a nightmare, I don't want to touch it ever again.

With Clojure you are done before Intellij has even rebuilt your indexes /s.


The JVM is a blessing and a curse. I believe it was a very good decision and that Clojure wouldn't have become as useful or popular as it is without it, because it provided immediate and quick access to a huge ecosystem, allowing it to be productive and useful right away. But its also hindering further adoption and may be part of what caused a bit of a decline in interest, which for an already niche language, doesn't help. Many people are turned off the JVM.

I personally don't mind it and I love Clojure, but even I have been wondering whether I should port some services to something else because I've had to use larger cloud instances for them simply due to memory requirements... Maybe I can run them on node with Clojurescript instead, I'll have to investigate that, but I think I'll lose out on a ton of libraries that I use that rely on the JVM. Hmm.


Scala can be enjoyable but can be a major pain as well. Look no further than the de-facto standard build tool (sbt).


I don't think JVM is what turns people off. It's a different paradigm. Every FP language is basically a niche these days even though you can hear great things about most of them(Elixir? Elm? Clojure? F#?). It's just that FP is not popular/common enough for these languages to be popularized like some other OO languages.


Elixir seems to be pretty popular here on HN these days.


HN is not a good representation of the programming world at all.


It does tell you what legacy code bases you'll be maintaining in ten years though ;)


The JVM is best in class, and has great ecosystem (with some noteable omissions).

Scala is a different beast: modern Java negates the need for a lot of it, and the community tolerance and support for bigots and abusers justifiably distances many.


According to LibHunt's popularity index, it's the 28th most popular language https://www.libhunt.com/index#clojure (ahead of Elixir that is 40th).

It is less than 50th position on Tiobe's index https://www.tiobe.com/tiobe-index/ (which is a bit more reputable than LibHunt's)


Yeah but does any programmer actually believe these indices? I have yet to see one with a prelude where they defined "popular" in a way that would make anyone with a passing knowledge of stats happy.


I've used Clojure for going on 10 years now and it has been one of the most enjoyable programming languages I've ever used. But I've gone back to Scheme for a few ridiculous reasons.

1. TCO. Using `recur` in Clojure just breaks my thought process. 2. Difficulty in getting down to the metal when needed. Using JNI is painful. In Scheme, when needed, the FFI usually handles plain old C. Much easier in my opinion. 3. Community. Rich is a really smart guy and has made great contributions, but he doesn't suffer fools lightly and I am a fool. Great for a scientist, not so much for a "benevelent dictator for life" of a programming language. 4. Pragmatism over correctness. There was a long running conflict about some set operations that did not return correct results. To my mind, there is no argument against correctness. 5. Slow startup. Not always a problem except when it is. 6. ClojureScript. Could be my unfamiliarity with ClojureScript/JavaScript, but I find I have to revert to JavaScript too often to get something done. And if I can't just use ClojureScript, why use ClojureScript?

Like any Lisp, Scheme is easier to read than Python, Java, JavaScript, Haskell, PHP, Perl, Fortran, Forth, APL, etc. Pascal is pretty good in this regard though. Clojure might actually be better in my opinion.

Why not Common Lisp? It's pretty good and at least as powerful. The warts annoy me though.

So I will continue using Scheme while looking into Janet https://github.com/janet-lang/janet. Janet has most of the things I like about Scheme _and_ Clojure. More experience will tell. I just miss the way maps are handled in Clojure.


Yes. Chez Scheme in particular is a remarkable implementation. Idris 2 for example (a leaner, dependently typed relative of Haskell) relies on Chez Scheme.

As a mathematician I don't need a large ecosystem of other people's work. What keeps drawing me back to Haskell is the experience of reasoning about code paradigms as if they are a branch of algebra (not everyone appreciates algebra, but those who do can never turn around), and a support for parallel code that is decades ahead of any other language I have tried.


I don't understand arguments of the kind: "Lisp A vs. Lisp B". A Lisp is a Lisp. I am honestly so grateful for my younger self for the decision I made years ago to learn Lisp. These days, pretty much any platform, any hardware, or a VM supports at least one dialect of Lisp. And that's awesome!

I switch between Lisps with relative ease: Scheme, Fennel, Clojure, Elisp, Common Lisp - they feel pretty much the same language. Yes, each has its own oddities and quirks, but I still feel like if I'd discovered some ancient secret - learn it and you start understanding every possible human language. That's how knowing and "breathing" Lisp feels sometimes.

Because Lisp is not a language. It's an idea. An incredibly awesome one. Arguably - the most influential idea in Computer Science. "Once in a millennia" idea.

That's why I never feel anxious about the popularity of programming languages. I don't care what's in the top of RedMonk or TIOBE or whatever else. I don't care if some new programming language gets promoted by the Queen of England, Supreme Pontiff, or Dalai Lama. If it's not a Lisp - it has an expiration date. Lisp, though, will never die.


I think of the "Lisp A vs. Lisp B" discussions like describing the nuances of you favorite color. My favorite color is yellow, but that covers a lot of ground. I prefer a nice bright lemon yellow, but don't care for mustard yellow or brown as much. But any Lisp is better than Fortran just as I prefer any yellow over purple, for example.

I prefer Scheme to most other Lisps because of guaranteed TCO, the way continuations are handled, regularity of function names, and define-syntax macros.

Within the Scheme family, I usually prefer Chez for its performance, its FFI and because I happen to prefer an R6RS compliant system at the moment.

Just nuances.


> I don't understand arguments of the kind: "Lisp A vs. Lisp B". A Lisp is a Lisp. I am honestly so grateful for my younger self for the decision I made years ago to learn Lisp. These days, pretty much any platform, any hardware, or a VM supports at least one dialect of Lisp. And that's awesome!

“Lisp” is no more a language family than “curly braces" is. Individual Lisps are often further apart from each other than either from various other languages.


> I switch between Lisps with relative ease

The only relevant measure is: how much actual code do you switch?

Because otherwise that's just like saying "I switch between brain surgery, jazz saxophone, chess and rock climbing with relative ease".


Curious, because I'm also doing Scheme now, and Clojure was my gateway drug, which Scheme are you using and for what sort of problem? I'm using s7, but that's because my use case is very much oriented to s7's non-typical feature set (it's computer music), but Janet looks really nice too. (Many similarities to both Clojure and s7 actually).


Mostly Chez. Racket is remarkable too -- I have never felt that I was stretching its capabilities. Enjoying Janet, trying to get over Clojure habits where the two differ.

Most of my career has been involved in medical diagnostic, data analysis, and desktop applications. Hobby projects include text processing, editors, outliners, wikis, and knowledge management.


Cool, thanks for the details.


Interesting, Clojure was my gateway drug to CL. I wonder how common it is for people who come to Clojure but get warned off by the JVM to migrate further down the lisp road?


yeah that was basically it for me. JVM for computer music just doesn't make any sense but I loved the language so went hunting for others.


Nice article.

Sadly a lot of people won't even try clojure since it is dynamic typed. I see their point but nevertheless clojure does something really well here. As the author obserserves designing around some core data structures results in high code reuse. A library like spec is also way better in encoding business requirements than all the mainstream language typesystems e.g. a number in business context has mostly constraints like a particular range etc. I really looking forward to see someone come up with a static type system inspired by clojure's approach.


PHP and JavaScript and Python are also dynamically typed the solution isn't yet another incompatible type system, there are so many type systems that don't work together, spec is also turing complete

Turing completeness in a type system allows you to use the type system as a meta coding language which is a fantastic amount of string to hoist yourself with, putting meta languages inside languages is a miserable place to be

I'd argue that most people just want the language to check for mistakes, clj-kondo is great and effective at doing this https://github.com/clj-kondo/clj-kondo/blob/master/doc/edito...

If you want types at your system boundaries like on your API then specs or malli is great for that

A great thing that Rich Hickey always advocated is separating out problems and then solving them individually I think modern type systems try and take on too many problems at once, static analysis tools are great because they're uncoupled by time and are laser focused on detecting mistakes only


Can we have a REPL-driven language that's statically typed?

My hope is yes, in that it's just that the work hasn't been put in yet to create to create the equivalent of Typescript for Clojure or Lua that compiles down to the actual, extensible language.

I always wish that it would become unnecessary to have to choose between stability and extensibility when selecting a programming language. Having a Clojure with static types or a typed Lua (Teal) would put us so much closer to that goal.


There's always Common Lisp using the popular SBCL implementation. It's not statically typed but you still get compiler warnings about types at compile time -- even cooler is that SBCL uses the types for assembly code optimization, so it can also guide you to write faster code with type warnings, e.g. about places where it couldn't infer the type and is forced to use a generic addition.

https://news.ycombinator.com/item?id=14780381 shows a few errors that provide typo protection, no type declaring needed

https://news.ycombinator.com/item?id=13389287 shows the optimization warnings

https://news.ycombinator.com/item?id=12222404 shows it can even delete unused code thanks to type inference

There's also a handful of attempts to bring stricter type checking guarantees on top as a library, but I haven't experimented with those. I'm very much in the crowd that doesn't find static types very valuable (apart from aiding performance) even for multi-million-line projects, but if I can get a few non-strict warnings like SBCL gives, that's a nice bonus, much like running a concurrent linter for extra warnings in <any lang>.

I liked Clojure a lot, but fell out of love once I realized how much it's still missing from the Common Lisp it was inspired from (non-bonkers OOP, condition system, and reader macros being the biggest to me), and also how its pervasive laziness is something I'd rather opt in to than vice versa. Some people feel the same about its immutability-by-default structures, I don't mind either way. (e.g. I can opt in to them in CL, that's fine, and Clojure's ways of opting out of them aren't bad either.)


As I said elsewhere in this thread, Haskell has a good REPL dev experience. Personally, I am a much better Lisp programmer (used Lisp languages since around 1978) than Haskell. My journey learning Haskell is almost a decade long, and still I am no where near as productive as using Common Lisp. I find that Clojure is usually a high productivity language, but I get stuck sometimes on platform details and that gets me out of a flow programming experience.

If you want to try Haskell, work though a few tutorials and if it looks good, you can grab a free copy of my Haskell book https://markwatson.com/opencontent/haskell-cookbook.pdf There are much better Haskell books than mine though, my book just reflects my own learning process.


Depends on what you mean by "repl-driven". If you mean a statically-typed language with a decent repl, then yes, you can have that--for example there are Haskell and ML implementations that give it to you.

If you mean "repl-driven" in a stronger sense, in the sense of a livecoding repl-driven environment that supports building programs by interactively modifying them as they run, then about the only place you find full-featured support for that style of programming is in old-fashioned Lisp and Smalltalk systems. Maybe also in Factor, and arguably in FORTH (though with fewer conveniences).

There's no reason in principle that you can't have a full-featured repl-driven environment for a statically-typed language; I just don't know of any. That's not a big surprise, though. There aren't all that many of them for dynamically-typed languages, either. It takes a lot of work to build one, and the builders pretty much need to know up front what it is they're trying to build, which means they probably need to have seen one before. Most programmers haven't.

Static types aren't particularly an obstacle, but strong immutability is. A full-featured repl-driven livecoding environment wants the programmer to be able to inspect and change anything and everything in the live environment as it runs. Soft immutability is fine; it's okay if you have to say "Mother, may I?" before changing something. Hard immutability is a problem, though. If it's actually _impossible_ to change something, that's incompatible with the nature of a repl-driven livecoding environment. A programmer accustomed to livecoding environments will experience that impossibility as a bug in the environment.


Typed Racket actually has a rather marvelous type system.

The following is fine therein and statically type checks:

    (let ([x (if (condition) "string" (list "a list"))])
      (if (string? x)
        (string-length? x)
        (length? x)))
By process of elimination, because the type of the variable is `(U string? (List-of string?))`, the type checker can prove that if `(string? x)~ be false, the type of `x` must be `list?`, and that `(length x)` succeeds.


Scala for example is statically typed and had a REPL from day 1.

However, when using a language with a great typesystem, the need for a REPL is just much less. You mostly don't need to try things out because the types guide you. Therefore these languages might have a REPL and it might be useful, but REPL-driven is not really necessary.

> I always wish that it would become unnecessary to have to choose between stability and extensibility

Stability, extensibility, low language-complexity. Pick 2.


I'd argue Clojure has all 3

Stability - one of Clojure's core tenets.

Low language complexity - not much more than EDN and everything is an expression, plus a few extras like destructuring. Certainly simpler than any other language I have used.

Extensibility - again, one of Clojure's strong points with macros.

As for "Type systems removing the value of a repl" again, I disagree. Repl driven development is as much about exploring the problem than it is writing code.


Stability in this context means "can make changes with confidence in the absence of tests". The more confident you can be, the more stable. Maybe not the right word though.

> Repl driven development is as much about exploring the problem than it is writing code.

Which is exactly what types give you. This even coined the term "type driven development" (same as "test driven development" on purpose").


> Stability in this context means "can make changes with confidence in the absence of tests". The more confident you can be, the more stable. Maybe not the right word though.

I'd call it "robustness".


You certainly can, OCaml and Haskell do. They were sadly not designed with S-expressions though.



Can you define REPL-driven here? because just to name 2, Scala and Haskell have a very good REPL.


Maybe what I should have said was something along the lines of an "iterative language." Essentially I was thinking of a language where you can reload the functions/types in a single module and see the results instantly without restarting. The shortening of the feedback loop is the important part. And also important is being able to take some code and put it in a REPL to find out what it does or modify it until it's just right.

One language which is explicitly designed to have first-class hotloading support and static types is Mun. It's not production ready, but it was born out of frustration over the dynamically typed nature of Lua while appreciating the benefits it brought to extensibility and rapid prototyping.

https://mun-lang.org/

For me Lua has the iterative part, but no types. The iterative part makes LÖVE an attractive target for gamejam developers, who don't want to be constrained by compile-restart cycles when they don't yet understand what every aspect of the finished game will look like.


I don't think it's about it's dynamicism but more about it being functional. Plenty of super popular dynamic languages out there. I think that's also what keeps elixir from becoming something more mainstream, most people come from OOP and are used to thinking about programming that way.


You're both wrong. If it was dynamicism then why are JS, Python, and Ruby so popular? If it's about being functional then why has Scala got more users?

Languages are driven by the platform. There is no Clojure platform that people want to use, so no one uses Clojure.

If a language isn't bound to it's own platform, it can share a platform and displace other tools like python, go, and rust do with C and C++ (docker is go; docker-compose, dnf is python; etc).

Scala has carved out pat of the jvm platform (spark, kafka). Clojure has not.

A common onramp is command line tools, but Hello world in Clojure takes 670ms to run. This is a total non-starter.

    time clj -M hello.clj
    Hello world
    clj -M hello.clj  1.05s user 0.12s system 175% cpu 0.672 total
Without an on-ramp to take over a platform, Clojure will not gain traction. Language quality is not a significant driver in adoption ; that's why shonky R has so many users. They will suffer a great deal to use dataframes and ggplot2.

If Clojure is so great, where are these cathedrals that people have made that should make it a no brainer to pick up Clojure? Around what are we circling the wagons?


Clojure has reached sustainable traction. There are large companies using it. There are shops that work with it exclusively. There's no need to conquer the world ;) If some users are evangelizing too loudly, well, it happens everywhere. You can choose to ignore it. And if you are curious why you can check it out and decide for yourself. No random person on HN can convince you.


>it's about being functional then why has Scala got more users?

Scala world is not that functional if you measure it by real-life usage at least. Most of Scala code I've seen was mostly OOP sprinkled with functional tricks.


Compile that same app with graalvm native and you'll get near instant hello world

If you don't want to have to manually compile your app then you can use https://babashka.org/

How many platforms do you think Clojure has?


> Compile that same app with graalvm native and you'll get near instant hello world

Could you show me how?


Your hello world actually took one second to start, add libraries for a web application and you will be hitting 2-3 minutes of startup time.


That's actually not correct at all. Hello World is slow because you have to start up the JVM. Once it's started it's rather fast. Your web application has the same JVM startup cost but not a lot more than that.


The jvm takes 50 ms to start. That's 620ms unaccounted for in Hello World.


Both of you miss the most obvious difference between Clojure and all mainstream languages: it's a lisp.

I've had so many programmers look at code I write and proclaim: "wow that looks impossible because of the parenthesis" and that would never touch anything like it because it seems so different.

Some people do take the time to learn how it works, but many just have a knee-jerk reaction to it and then forget about the language itself.


I have a theory: it's because Blurp progammers learn to equate parentheses with complex (mathematical) expressions. The view of many parentheses at once must trigger an acceleration of their heart pulse.


It’s just like C or JS but the first paren goes to the left of the function name.


With function calls, yes.

What makes it different is that syntactic constructs are also expressed with the same syntax and there is no special syntax, so control flow constructs use a syntax similar to function calls.

This is particularly exuberant in Arc where what in many languages would be:

    if        (<cond1>) {
      <then expr1>
    } else if (<cond2>) {
      <then expr2>
    } else if (<cond3>) {
      <then expr3>
    } else {
      <else expr>
    }
Would instead be:

    (if
      <cond1> <then expr1>
      <cond2> <then expr2>
      <cond3> <then expr3>
      <else expr>)
Note the complete lack of syntax beyond a keyword having 7 arguments in order, which many find nonconsecutive to reading, and also error prone as accidentally making a typo can completely change the meaning of the program.

It is thus that most Lisps have somewhat more redundant syntax:

    (cond
      [<cond1> <then expr1>]
      [<cond2> <then expr2>]
      [<cond3> <then expr3>]
      (else    <else expr>))
Personally, I wouldn't mind that a mandatory `->` be required in between the conditions and the expression to further guard against accidental typos. I find that redundancy in syntax guards against accidental mistakes, though I have nothing against the parentheses and in fact favor them.


Pattern matching in rust:

  match x {
      <cond1> => <expr1>,
      <cond2> => <expr2>,
      <cond3> => <expr3>,
      _ => <expr>,
  } 

  let message = match maybe_digit {
      Some(x) if x < 10 => process_digit(x),
      Some(x) => process_other(x),
      None => panic!(),
  };
Seems useful. Python's getting match soon too!

https://doc.rust-lang.org/reference/expressions/match-expr.h...


Clojure specifically doesn't have the bracket wrapper around condition-expression pairs but syntax highlighting/ formatting considers it and aligns stuff nicely. I don't think, it is a problem that it basically is a list or function call with arguments. I do think, adding `->` just like that would be a problem because it would be very inconsistent with the syntax of Clojure.


Well yes. Clojure in general tends to have less special syntax and in general is designed very consistently. What I find very useful is the threading macro ->> and -> which basically is a pipeline:

(-> something (manipulation-of-something) (nextmanipulation) (anothermanipulation))

instead of:

(anothermanipulation (nextmanipulation (manipulation-of-something something)))

Often, you can remove some parenthesis in the first case as well.

I really love this. It is also very easy to just wrap it in a let or a function. This alone should open the eyes of anybody, who has written at least some Shell script somewhere or does some data analysis. If it doesn't, maybe the person isn't actually that great of a thinker or a practitioner and you would be better off around other people professionally. Frankly, who are the people, who cannot grok moving the opening parenthesis before the method/ function? I don't think I have ever met anybody like that - I only read/ hear about such people in comments or at conferences and I haven't heard a name yet.


I don’t think it’s the parents, but the s-expression themselves.

LISPs forces you to maintain the stack for the parse tree in your head, something humans aren’t that great at — s-expressions are the programming language equivalent of center embedding, which is quite alien for human languages (the depth is three at most: compare that to your favorite lisp program)


If you've seen 300+ line react components marching off the right of the screen, you'll know that maintaining the parse tree in your head isn't a barrier to popularity.


While this made me laugh a bit, I think there's a meaningful difference between using "tree-like" syntax for all your code (lisp & S-expressions) vs declarative UI descriptions (JSX).

A deeply nested syntax is beneficial for UI work because you can correlate the structure of the code with the interface/document being rendered. S-expressions for HTML/UI in the form of Hiccup-style templates are equally good (if not better) for the same reason.

In JSX however, there is a clear syntactic distinction between behaviour (C-like JavaScript syntax) and interface descriptions (HTML-like element constructors). In Lisps, the uniformity makes it harder to quickly distinguish "behaviour" from "data", which is kind of the point, but comes with a trade-off in readability.


> In Lisps, the uniformity makes it harder to quickly distinguish "behaviour" from "data", which is kind of the point, but comes with a trade-off in readability.

Maybe in theory, but in practice it's easy to tell because in the case of hiccup, the data is data (as vectors) and behavior is behavior (as function calls), those have different syntax in Clojure.

Lisps are excellent at manipulating trees, which is exactly what HTML and the DOM is.


> you can correlate the structure of the code with the interface/document being rendered

Note that you're taking for granted that the document must necessarily be a nested tree structure, which then behooves us to follow in the code.

Which may be right and good (and in any case is foisted on us); but the assumption bears spelling out.


300+ line components are not popular


Not with me they're not, no.


You might be right in the first part, and the "fear" of s-expressions are only expressed as a fear of parenthesis.

But on the second part I think that's the same as for most languages. You end up with nested scopes at the same degree as any c-like language really. But most lisp programmers tend to break out into new functions a bit earlier than let's say JS programmers. I'd argue that normally you'd have to keep track of less depth in a normal Clojure program than you would in a JS program, simply because of how a programmer usually works in those languages.


That's true if you write everything on one line. But how could you fail to notice that most Lisp code is written on multiple lines and indented? The structure is visually laid out, so as not to be maintained in anyone's head.


Line breaks only shorten lines, they do not change the reading direction. Yes, you can start at the bottom and read upwards, but that's unnatural for most.

Compare:

  something.first().second().third()
With:

  (third
    (second
      (first
         (something))))
The end from read must you to understand, and it gets more complicated as your code does. No wonder Clojure's threading macro is so popular, as it would allow you to write it as:

  (-> something first second third)
Fun fact: Lisp was never supposed to be written with S-expressions. They were intermediate representation, for bootstrapping. McCarthy designed M-expression, with function notation, inflix, and sugar'd cond and list; but all that was omitted due to lack of time, and we were left with S-exps.


> Lisp was never supposed to be written with S-expressions. They were intermediate representation, for bootstrapping.

That's not the complete picture.

The early Lisp manual had a definition for Lisp syntax. The Lisp syntax was based on M-expressions for code and S-expressions for data.

Basically what now is

    (append (quote (1 2 3))
            (quote (a b c)))
was

    append[(1,2,3);(A,B,C)]
where the function call uses M-Expression syntax and the data were S-expressions.

Then we have so-called S-Functions, which work with S-expressions. append is such an s-function.

McCarthy then defined a mapping from M-Expressions to S-Expressions, thus that M-Expressions could be represented (not just written, but also in memory) as S-expressions.

In the next step he defined new S-functions called apply and eval, which took M-Expressions as S-Expression data and computed the results of apply or eval operations.

Example use of apply:

   apply[(LAMBDA,(X,Y),(CONS,X,Y));((A,B),(B,C))]
Thus these s-functions could compute with code which was represented at runtime by s-expression data.

Thus such a program would use both code in M-Expression format and compute with code in S-Expression format.

Since these S-functions apply and eval could be themselves translated to s-expressions and get executed, the specific S-functions apply and eval could get executed by a s-expression evaluator.

The code above would then be written:

    (APPLY,(LAMBDA,(X,Y),(CONS,X,Y)),(QUOTE,((A,B),(B,C))))
which in modern way would be written as

    (apply (lambda (x y)
             (cons x y))
           (quote ((a b) (b c))))
Since programs thus were executed / computed as s-expressions, they were input, computed and printed as s-expressions.

Thus the idea of a simple s-expression meta-programming system made the idea of an additional step of m-expression syntax reading/printing less attractive.


You have a strawman example of piping via .member() because those () sometimes have arguments; that's what they are there for. Function application has not gone away; it's just combined with obj.member access. It can easily become an unreadable mess that will need some way of splitting across lines and indenting:

  something.first(other.foo(bar.f(x, y)).memb, z).second(x.y()).third(a, b, c)

This:

  (third
    (second
      (first
         (something))))
is just function notation with the location of the opening parenthesis having been re-examined, and commas removed. Function application notation is found in a myriad languages: sin(cos(pow(x, 2))).

With the above indentation, it's very readable to me; it's very clear that calculation starts with (something) and moves in an outward direction.

  (-> something first second third)
Right, yes, so we have threading macros, and people use them. That's not all that goes left to right. Lisp's ancient progn (including implicit progn) goes left to right, as do the arguments of functions and most macros:

  (defun app ()
    (init)
    (event-loop)
    (cleanup)
    (exit 0))
Sequential binding can break up a nested "point-free" expression, as an alternative to threading:

  (let* ((a (first (something)))
         (b (second fi))
         (c (third se))
     ...)


Of course you could work around s-expressions (and make your ALGOL-formatted language looks like Lisp, a common complaint against my own code by my coworkers), that wasn't my argument.

Most aren't fond of Lisp syntax, regardless of how you dress it up, and thus writing in Lisps doom you to have fewer people to hand over your code to, and I don't think that scarcity is useful. I suspect the reason most don't like s-expression is that it forces the human reader to maintain a "mental stack", an exercise humans are not too good at, as demonstrated by human languages aversion of center embeddings.


You seem to be stuck on this idea of deep function call nesting being an impediment, which is solved by foo.bar().baz() chained syntax using object dot notation.

But most mainstream languages have chained function call notation as a feature.

Furthermore, foo.bar().baz()... chaining is a fairly recently emerging idiom. It has been possible in a number of languages for decades already, but somehow didn't take off. You would hardly see chains of foo.bar().baz().xyzzy() in 1990 vintage C++ code bases, even though 1985 vintage C++ would easily support it.

Anyway, there is a dialect of Lisp which has integrated the dot syntactic sugar into S-expressions, according to this basic idea:

  This is the TXR Lisp interactive listener of TXR 257.
  Quit with :quit or Ctrl-D on an empty line. Ctrl-X ? for cheatsheet.
  Poke a few holes in TXR with a fork before heating in the microwave.
  1> '(quote x) ;; i.e. just like we have a 'x -> (quote x) sugar ...
  'x
  2> '(qref x)
  (qref x)
  3> '(qref x y) ;; we can have a x.y -> (qref x y) sugar
  x.y
  4> '(qref x y z)
  x.y.z
  5> '(qref x y z w)
  x.y.z.w
  6> '(qref x y 3 w) ;; (let's not when it's ambiguous with floating-point)
  (qref x y 3 w)
  7> '(uref x)  ;; ... and a .x.y (uref x y) sugar
  .x
  8> '(uref x y)
  .x.y
  9> '(uref x y z)
  .x.y.z
  10> '(uref x y 3 w)
  (uref x y 3 w)
Embedded compounds are possible, of course:

  11> '(qref (a) (b) c (d) e f (g))
  (a).(b).c.(d).e.f.(g)
I never intended this to be used for chaining! In fact, only in a fairly recent update to the list-builder object, did I fix it so it can do this:

  12> (new list-builder).(add 3).(add 4).(pend '(5 6 7)).(pend 8).(get)
  (3 4 5 6 7 . 8)
Needless to say, the methods have to return the object in order to make this possible. Before the update, the methods didn't have a specified return value.

In the first place, list-builder is an implementation mechanism under the build macro, which expresses it like this:

  13> (build (add 3) (add 4) (pend '(4 5 7)) (pend 8))
  (3 4 5 6 7 . 8)
so there is no reason to use list-builder directly in most code.

This chaining business is a minor benefit (if at all) of the dot notation. The main motivation is to make programming with structures and OOP more ergonomic. It has a big impact for programs that use data structures, because the use of data structures and OOP can pervade the entire program, and is a driver behinds its structure.

Anyway, anyone discussing S-expressions under the assumption that they do not have a dot notation that can be used for function chaining is simply unaware of the research having been done in this area in the TXR Lisp project.


I love that you use 4 indirections before getting to you main, parenthetical, point.


Can you blame them though? Look at Python/Ruby where even non developers can sometimes understand the code and then look at some crazy Haskell/Lisp expression.


Yes, I do blame them for this because if you spend 10 minutes, any programmer can learn "some crazy lisp expressions" because they are not that crazy.

This knee-jerk reaction you're having is exactly the ones I'm talking about :)


So you're saying there's absolutely no loss of readability in something like Lisp opposed to Python?


Is there? I'm not convinced.

I think a lot of the readability issues come from lisp syntax being unfamiliar, not from some intrinsic impenetrability.

And then even more from being associated with functional programming, also unfamiliar to a lot of people.

Look at common lisp nested for loop[0]. Are they really that hard to read?

And then there's the minimalism of lisp syntax. Once you know how to call a function and the few data literals (list, vectors, set, ...), you know 90+% of the syntax.

Compare with python, where you also need to learn class syntax, annotation syntax, for loops, if, while, comprehensions, etc.

And the list keeps growing, there's new features being added continuously. In lisp, if the language introduces a new construct, it's still going to be just symbols in between two parenthesis.

[0] https://lispcookbook.github.io/cl-cookbook/iteration.html#lo...


I'd argue that because of the lightweightness of Lisp syntax compared to C-like languages, Lisp languages are easier to read. But then I'm a professional Clojure developer who used to do Golang, JavaScript and some other languages, but since couple of years ago only do Clojure and ClojureScript development, so I'm obviously biased.

But then Python is probably the worst example to compare Lisps to, as Python sees whitespace as a significant character that can affect if the program can run or not.


I'm sympathetic to the parents that it's not as bad as people say, but I'd definitely agree with you that syntax at least to me provides a lot of value. Of course, too much syntax is a problem in the other direction.


I really like Clojure, it's the language that finally made FP "click" for me. It was my go to for hobby/side projects for quite a while.

Dynamic typing is why I eventually switched. Haskell scratches the same itches that Clojure did, but the compiler and type system are immensely helpful, and keep saving me from tripping over my own feet.


Common Lisp is my go-to language, but I also like Haskell and Clojure. To me, Haskell is a Lisp, and supports REPL driven development.


Somewhat off topic: My problem with Haskell is that every time I've tried to read the documentation, I've felt like I needed a PhD in type theory to understand all of the terminology. As a practitioner (not a researcher), I just want to know how to do things, but the documentation has always been a roadblock to me. So, after a number of attempts at learning to use Haskell, I've decided its not for me. Not because of the language itself, but because of the traditions around it.


This. I think I understand the basic concepts, but you get a first big slap with doing your first curl to some other service. Ergonomics of the libs is often times horrible. You are in this constant loop, oh I can't do this I need algebraic derationalizer to align those types. Several hours later your curl request works. You start looking at wall to decompress. Curl is a pretty good example its a complexity that has been made super easy in a lot of languages.

I am very much waiting for some "extremely" constraint subset/flavour of Haskell that gets some adoption(I do not think I am alone in this lobby). No crazy stuff, no "just read the types", no "its just a small extension". But I also know that it is not really feasible without breaking the IO enforcements.


Finding staff is an interesting negative of Clojure to mention.

I searched for a Clojure job off-and-on for several years in the 2012-2018 timeframe. I had attended the very first Clojure/conj in 2010 and had development projects in production very soon after that - so I had some Clojure specific experience and was otherwise a reasonable candidate.

My opinion was that there were far fewer opportunities than the inner core of the Clojure dev world admitted. This opinion seems very much not shared or mentioned by others. I also think that Clojure opportunities seem more limited now than a few years ago - Clojure seems to have become one of those programming languages listed on job descriptions but Clojure isn't actually part of the job.

Never getting an offer from a Clojure-oriented job was kind of weird to me at the time because I did get lots of interviews outside of the Clojure world and pretty regular job offers from those. I never figured out why I couldn't catch an offer from a Clojure dev org.


I posted a couple years ago about my experience finding Clojure jobs in 2018. https://news.ycombinator.com/item?id=20075132

I still have about 10 recruiters a week mail me about jobs relating to Clojure (either Clojure roles directly, or someone looking for a "functional" developer for another fringe language). It's unclear how many of these are the same handful of companies cycling recruiters (or with open reqs). At least in Chicago, it seems pretty straight-forward to get a job hacking Clojure if you have prior Clojure experience.

The other alternative is bringing Clojure to an org. You solve problems and a lot of orgs honestly don't care about the details, particularly if you're in a silo or one of the only devs. This is harder to do in an established org that already has templates for specific things, unless that org is big into containers or microservices already. I've never been hired for a full-time job as a Clojure developer (although I've had offers), but I've put Clojure into production across three organizations.


Interesting perspective from your 2018 comment - thanks for linking it.

I am fascinated by the difference between your experience and mine when hunting a Clojure gig.

When I made my comment, I was wondering how quickly someone would make exactly the response you made - no judgement intended. It's been the standard response ("I easily got a Clojure job ... <details>") I have seen for years now.

It is just a very different experience than mine. I would love to have insight into the raw numbers of (devs-who-got-a-Clojure-job / devs-who-want-a-Clojure-job).

I will also second the insight that you can bring Clojure into an org as a problem solver and did exactly that in the early days of Clojure precisely because I was in a mostly silo-oriented organization.


If I want to learn a new technology, that's been out a while, I make sure to look for jobs for it first. Just a gut check on how many opportunities are out there for it.

This doesn't work for something brand new of course, but I still do a cursory look through job sites to see what I can see.

Like for instance on Dice.com right now there are 60 jobs available. If I were interested in Clojure today and saw that, I probably wouldn't bother because I know Clojure is mature but there's not a market for it apparently.

Again, this isn't meticulous or scientific at all, just a gut check with a few searches.

Then again, Rust only has 13 jobs listed soooooo.....


I really enjoy the language and even the look of it, and I have tried several times to use it.

For "simple" scripts or server processes that you might call from cron, it was fine. But for web development, it was always so hard to get up and running - get everything configured and then know how to do things - that I always gave up. The Clojure community really values choosing libraries and tools and seems to echew Rails-like approaches. That's good if you know what you're doing, but it makes the cost of entry really high. And for better or worse, Ruby is a pretty nice language and Rails is pretty easy to get rolling with... so it's very difficult to justify the effort of switching. I want to... I really do. But at this point, unless I work for a shop like TFA, it will just never happen for me.


I've had some difficulty getting a "basic" web server up and running too. Composing libraries together is a great way to build software, but when you're starting from such a low level with a new language where that kind of development style is the norm, it's a steep initial learning curve.

Have you looked into Luminus? I haven't tried it yet, but it's supposed to be a Rails-like starter kit for web apps: https://luminusweb.com/


I actually did build something basic following the book Web Development With Clojure (https://github.com/jumarko/web-development-with-clojure), but honestly the way the code snippets were presented in the book were sometimes difficult to use if trying to follow along and build it yourself.

I think you could probably get code from different chapters from their git repo, but then you're not doing it yourself - you are instead trying to read diffs to understand what's new and figure out why it has been changed.

My experience has been that there is no complete guide, tutorial, or example that is kept current and provides every detail such that you can follow it and learn. I'm sure with enough concerted effort, one obviously can learn it... but there will be some trial and error and some guesswork. Normally that's fine, but it slows the process compared to other tech stacks and their guides.

The Clojure community is nice and helpful, but they're all busy doing real work (rather than teaching). Even the book I mentioned is not yet complete and has been in progress for over two years I think.


I agree. I love using the language for smaller things, but since I don't use it at my day job it's really hard to get well acquainted with the common tooling/libraries. When I try starting a new project I always end up spending hours just getting my environment in a good state.


I wonder if the company's success is not because Clojure is a marvelous language for building systems, but because Clojure is a marvelous people filter?


This is a great article, and I think does a great job of surveying the landscape as it stands. Particularly great to hear reports of ease in hiring and training, which doesn't surprise me, but is nice to be able to cite when risk-averse tech leads express concern about staffing and team-scaling.

I find it somewhat funny that a lot of the critiques here are about purity or types. I really see it as Clojure's core strength that it's both pragmatic and approachable. I'm a math/alegbra nerd, so I love languages like Haskell & OCaml, but they're much more austere for the uninitiated, and have had their own troubles growing. From my experience, the approach of Clojure spec is really nice because it gives you more flexibility/freedom/support in both thinking at a high level about the design of your system (in spec code) before you actually write the (implementation) code itself, while also not requiring it, and letting you add after the fact. You also get a lot for it: generative testing, destructuring, documentation, etc.

That having been said, I'm surprised that more of the pro-type folks here don't seem to be familiar with the [typed clojure](https://github.com/clojure/core.typed) project, which adds optional typing, along the lines of Racket's contract system. To me, it's one of the great testaments to the power of Lisp that you can fairly easily implement types on top of the core language.

My one beef with this article is that scripting with Clojure is now not only possible, but a joy, thanks to GraalVM and [babashka](https://github.com/babashka/babashka)! This has made a HUGE difference in the surface area of problems to which I'm able to apply Clojure.


Very nice post! I wonder why the article date is in the future (10th May 2021)? :P


Anybody here mixed code base between clojure and another language to have a concise / dynamic layer with some 'classic' statically typed modules underneath ? kotlin/java comes to mind due to JVM but maybe others.


Java and Clojure are probably pretty common. I sometimes use the Clojure numeric stack and Java (JavaFX) for the GUI portion of a program.


Lost me at "the promise of purity". Nobody wants to deal with high priests, just get the job done!


That's... exactly the point of that section. You should continue reading the part right after the section title you mentioned.


Well, no not really. They started with the conclusion that being a clojure geek is cool and then try to post justify it. Good programming practice isn't a language feature its a programmer feature.


> Good programming practice isn't a language feature its a programmer feature.

I'll enjoy reading your production-grade brainfuck code.

A language's built-ins and idioms greatly influence what you say and how you say it. If your mother tongue doesn't have words to describe any emotions you'll have a hard time explaining them. You're talking only about the other side of the coin. Yes, people can write terrible clojure code, you need to be a good programmer to write good code. But please consider rewriting this bash one-liner in pure C (or assembly)

    grep FOO */* | wc -l
Having a language to express your domain concisely is essential in reducing code complexity, time-to-market, bugs etc.


Do you really think referencing brainfuck, a deliberately difficult language, is the right way to get your point across?


> > > Good programming practice isn't a language feature its a programmer feature.

> > I'll enjoy reading your production-grade brainfuck code.

> Do you really think referencing brainfuck, a deliberately difficult language, is the right way to get your point across?

I'm gonna assume you're not deliberately missing the point here and the confusion is genuine.

You were initially indirectly saying that "it doesn't matter what language you use, how the program ends up is 100% to the developer"

The other person made a joke/insight about that, since you think so, you should be able to write a program ready for production in Brainfuck, since your point was that the language doesn't matter.

Obviously, anything but Brainfuck would make it easier to both write and understand, therefore your point seems to not be 100% true, at least always.

The main point is not about Brainfuck, it's just an example. The point is about that the programming language _does_ matter, as well as the skill of the developer.


Brainfuck is not an example, its a strawman. If it was meant to be a joke, it sucks.

I really don't understand the automatic genuflection that lisp/forth/smalltalk get on HN. You can't make a bad programmer better by switching out the language.

I am guessing HN skews younger and never had to build boring production systems in these failures (note: my professional experience was years of fighting smalltalk).

For the vast majority of programming tasks, boring old java or c# or python have enough language features of the holy trinity now that smugly announcing to the world you are a clojure exclusive geek is a huge red flag.


> You can't make a bad programmer better by switching out the language

Right; good programmer, though, another story.

> boring old java or c# or python

Many programmers using these were switched to them from something else, and in many cases are doing better work because of that.


If they moved on from lisp I'm 100% sure they are now able to do better work, largely because they no longer have to spend their days telling everybody they are a lisp programmer.


Clojure is actually very practical and pragmatic.


Eh? That's precisely what that section is about, and it uses "purity" in the specific programming sense.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: