return to table of content

Why is Jepsen written in Clojure?

jwr
79 replies
1d14h

As a heavy user of Clojure (my SaaS is written in Clojure/ClojureScript) for 10 years or so, I would fully agree with what Kyle wrote. In addition, things that I found very valuable:

* most of my domain code is in cljc files, so these get compiled both server-side and client-side and I get a huge boost from that

* transducers and transducer pipelines are an under-appreciated feature, I get huge mileage out of them, because of performance, composability and reusability

* stability and longevity: Clojure was created by a very experienced system designer and programmer (Rich Hickey) and it shows. Backwards compatibility is hugely important, and I can focus on my application rather than rely on tools that are a moving target. It's not an accident that an average "Clojure programmer" (I dislike these kinds of tags, but let's run with it here) has 12+ years of experience and earns a salary in the top quartile at least.

DeathArrow
34 replies
1d13h

Does Clojure have a decent web framework?

jwr
16 replies
1d12h

I am not sure what a web framework is, to be honest. The choices for many parts of a web application are really domain-specific and I'm not sure a single "framework" would work for everyone.

As far as web-related components go, my app uses Rum (as an interface to React), ring, http-kit, pushy (for history manipulation), sente (for websockets), buddy (for authentication tools).

If you are looking for a batteries-included "I want to have some sort of webapp right away" thing, I think https://kit-clj.github.io would fit the bill, but the general feeling in the Clojure community is that unlike Python with Django or Ruby with Rails, the choice of app components is not predetermined by the language.

shortrounddev2
10 replies
1d8h

Components of a web framework:

1. Routing

2. ORM

3. Auth handling

4. Request middleware

5. Templating (if your application requires it)

6. User input validation

Many other components that are escaping me right now

WJW
5 replies
1d7h

Some more standard components I would no longer want to do without in a full-featured framework:

- background job system

- email sending

- translation/localisation

- a decent configuration system suitable for differentiating dev/test/staging/prod/etc, and handling both secret and non-secret values in a sane way

shortrounddev2
4 replies
1d6h

Secret handling has been abstracted away from the framework for everything I worked on; usually our deployment system would mount secrets as a file system and then you read them as a file of key->value pairs. That, or they were just set as environment variables. I guess the framework could provide a standardized secret service, but I usually just write that myself

WJW
3 replies
1d6h

but I usually just write that myself

Not saying you shouldn't, but part of the point of a framework is all the things you don't have to write yourself.

What I like about (say) Rails secret handling is that the secrets file is an encrypted file in the repo itself. This makes it so that you can easily check out and run different versions of the repo from different times and not have to worry about that one time you renamed an envvar or something. Secrets get version controlled together with the app. The autogenerated .gitignore even includes patterns for the key files so you have to go out of your way to check them into the repo. The downside is that merging encrypted files can be a right pain in the arse, for example when two devs added different secrets in different branches.

shortrounddev2
2 replies
1d5h

I feel that web frameworks should be flexible enough that the CI/CD system can put things like secrets into files without worrying about the particular framework that's consuming them. If you don't write the secret service in the web framework, you have to write the secret manager in CI/CD to convert formats or what have you. Something I dislike about spring boot is how spring-y everything has to be.

ElectricalUnion
1 replies
1d4h

CI/CD system can put things like secrets into files

This sounds horrible. Why does specifically your CI/CD need to know those things?

This also sounds like your CI/CD system is "too smart" and your build process is "dumb pipes", what I believe to be a unsupportable, unmaintainable anti-pattern - if people can't build/test things on their own machine, things are likely not repeatable/reproducible at all and you're one CI/CD issue away from losing your project.

Something I dislike about spring boot is how spring-y everything has to be.

I thought spring "runtime config override" was just dropping a properties (admittedly awful format, latin-1 encoded to boot) file near the compiled project uberjar/war/ear and you're done?

shortrounddev2
0 replies
1d4h

This sounds horrible. Why does specifically your CI/CD need to know those things?

You set it up so that production secrets are kept in that store and are only accessible by a 1 or 2 high level people. Developers don't have access to production secrets, only dev or staging, which live on their own machines and are lower security risk

if people can't build/test things on their own machine

I don't see why they wouldn't be able to build/test on their own machine. You just have a different secrets store and keys than the CI/CD does

I thought spring "runtime config override" was just dropping a properties (admittedly awful format, latin-1 encoded to boot) file near the compiled project uberjar/war/ear and you're done?

Yes I hate this. I want to be able to decide how properties are loaded, not rely on some ethereal state managed behind the scenes and not be able to see if it's loading properly. If I want to use their terrible resources/config file, I'll specify that in middleware/startup

jwr
0 replies
12h12m

Oh, that's a great list — see, that's why we disagree on the utility of "web frameworks". My app does not use a routing component at all, and none of the existing solutions fit my use case. I also don't have an ORM and I don't think even having an ORM is a good idea (I'm currently working on moving to FoundationDB, where you can get extreme gains by coupling your app closely with the database, rather than talking to it through a thin straw).

I don't use templating, at least not for web pages.

User input validation — that's something I've spent the better part of several years working on, but in a larger context of a toolkit for dealing with forms, tables, data presentation, filtering and sorting. I daresay the solution I have now (which uses ideas dating back to Smalltalk) is way more extensive and flexible than what every "framework" out there has.

abc_lisper
0 replies
1d4h

Most of these are handled by reitit and ring

PaulHoule
0 replies
1d8h

I think routing makes the minimal viable "framework" -- look at the many systems like Sinatra, JAX-RS, aiohttp, etc where you write functions and supply metadata to tie URLs to functions.

Capricorn2481
0 replies
1d7h

For clojure, that would be biff

spelufo
3 replies
1d10h

What about the database side of things?

stefcoetzee
0 replies
1d9h

next.jdbc is the go-to library to interact with SQL databases. https://cljdoc.org/d/com.github.seancorfield/next.jdbc/1.3.8...

simongray
0 replies
1d2h

Probably worth mentioning that triplestores/Datomic-like databases are relatively mainstream in Clojure: https://github.com/simongray/clojure-graph-resources#datalog

jetrink
0 replies
1d9h

If you don't need something as low-level as next.jdbc, HugSQL is my preferred library. It lets you write queries in .sql files, and the queries are then loaded as functions. If you want to dynamically generate queries, there are also libraries like HoneySQL.

22289d
0 replies
1d11h

I'm not sure a single "framework" would work for everyone.

I guess you weren't lying because that is not what a web framework is.

If it works for just me, that's a web framework. The fact that there are so many web frameworks shows that none work for everyone.

dgb23
5 replies
1d10h

Comprehensive frameworks include: biff, kit.

Both of these rely on some of the most popular and simple libraries for web dev.

Most seem to simply use said libraries directly, mix and match what is needed.

If you’re an experienced web developer but new to Clojure, then you’re going to spend a good weekend or so to set up a proper environment, combine the necessary libs and so on, if you have some guidance. The Clojure community is quite welcoming and helpful in that regard.

There’s an initial learning curve, because it’s a functional Lisp though. That comes with some necessary re-wiring, but also with some unique powers.

The cool thing about writing Clojure is that feeling: „Really that’s it?“ when you realize how simple it is. It’s also simply fun to use, because of how interactive it is.

digdugdirk
4 replies
1d9h

Do you have any recommendations for good resources for a starting point? And maybe something like a generic "to-do app" style tutorial just to see how everything is laid out and how everything connects?

dgb23
1 replies
1d9h

There are many good tutorials, free and paid books.

However, if you just want to get started and have a minimal thing working without much fuss I recommend this one:

https://clojure-doc.org/articles/tutorials/basic_web_develop...

I haven't directly used it but I reviewed it once and it seems to be one of the most straight forward and carefully written web development tutorials for Clojure. It uses a very minimal set of libraries and it really goes from A to Z. Perfect if you want to just get yourself familiar and have something working.

digdugdirk
0 replies
18h1m

Wow. There'll definitely need to be some translation before I understand everything, but you're absolutely right - this tutorial is one of the best I've ever seen.

Thank you for sharing it!

stefcoetzee
0 replies
1d9h

Consider the tutorial on the Kit website: https://kit-clj.github.io/docs/guestbook.html#guestbook_appl...

sometimesweenie
0 replies
1d7h

If you want a solid structure to start with, I’d suggest https://luminusweb.com/ as that’s what I initially learned from. For a todo app I believe the reagent repo has an example of that without the server bits. I could give you some more direction depending on what you’re trying to accomplish

gleenn
3 replies
1d13h

Luminus has been around a long time and Kit is newer. Most Clojurists believe in libraries over frameworks though so that's why you don't hear about web frameworks much. I've messed with Luminus a few times and it is really easy to pull out parts and swap in almost any bit of it. They document a lot well and make it really easy to pick different parts out of the gate like different DBs, JavaScript libs, and a lot more. The maintainer roams HN frequently as well :)

gengstrand
1 replies
1d5h

Whatever happened to ring or donkey?

gleenn
0 replies
1d5h

Luminus uses Ring.

aphyr
0 replies
1d8h

Aphyr.com is built with Luminus. :-)

rafaelmn
1 replies
1d9h

Because of it's lisp nature with special braces I find it to be nicer for creating DOM than HTML :

   [:div
     [:p "I am a component!"]
     [:p.someclass
       "I have " [:strong "bold"]
       [:span {:style {:color "red"}} " and red "] "text."]]
really has that "first class" feel when you're working with it rather than feeling tacked on like JSX - at least that's how I remember it.

My main problem with Clojure was the dynamic nature of it. I have not used it in very long time now (probably 7 years at this point) - but despite the amazing aspects of it I would not trade in TypeScript annotations.

A question for clojurists here - what is the level of static type checking adoption in CLJ world ? When I was using it last time there were some things like schema validators, but nothing like TypeScript. It looks like there's static typing annotations support now but are the types widely used by popular projects/provided for big libraries ?

jwr
0 replies
12h18m

From the point of view of a solo developer maintaining a large app over many years: I would not want to deal with static type checking and I would not find it useful, except for simpler subsets of my code.

I do use spec, and I've used other checking solutions before. I also use :pre/:post conditions a lot, as well as plenty of assertions. But in a large and complex application the errors I'm likely to run into are not as simple as a mismatched type. My most valuable checks and specs constrain multiple data fields.

Thinking about stuff I run into, I would say that the majority of errors are due to optionality — where a key is optional in some contexts, but mandatory in others.

yogsototh
0 replies
1d5h

Let me add my 2cents on this subject.

I feel that due to its functional nature or perhaps this is just the community bias, Clojurists tend to prefer mixing lower level libraries to build their web application instead of relying on a specific big web framework like RoR for example. Luminus would be one RoR-like system.

But today, I think people would probably choose reitit for the writing the API and use a clojurescript framework for the frontend or perhaps still use reitit to generate HTML pages using hiccup.

olavfosse
0 replies
1d12h
finnjohnsen2
0 replies
1d9h

"No, but maybe" is how I would answer this question myself. Maybe there is a working framework I'm unaware of - but in my experience using Clojure the last few years is that the concept of frameworks is not in the culture of the Clojure community.

You compose your stack of smaller libs and build your own framework so to speak. The right questions in this context are; what http server to use, what routing lib is decent, what is a good lib for my database, what is a good lib for generating html from the server. etc.

If you wanna start using clojure as a web server. Start with 'ring', and you can recieve http requests. You quickly realize you're missing stuff to do what you need and will go searching for libs. You'll find great libs out there for anything you ordinarily need for a web-server setup.

epgui
0 replies
1d11h

I know you’re probably going to think this answer is crazy or stupid, but you don’t need a web framework in clojure. In fact you don’t need all the libraries that many languages need.

daveliepmann
0 replies
1d11h

It has several. None of them dominate; most people assemble their own from individual libs.

drikerf
33 replies
1d14h

Similar story. I'm also running my SaaS on Clojure and maintenance has been a dream compared to other languages. Things rarely break and are easy to upgrade.

Developer experience is fantastic too with HMR, REPL and cljc files. It's fun to write Clojure code :)

weikju
18 replies
1d12h

HMR?

G4BB3R
17 replies
1d12h

"Hot Module Replacement (HMR) is a feature in some programming languages and frameworks that allows developers to update modules or parts of the application in real-time while the application is running, without requiring a full page refresh. This is particularly useful during development as it speeds up the development process and helps maintain the application state."

weikju
16 replies
1d11h

Hmm I’d rather hear more from the GP (assuming it’s the same concept) and specific tools being used than a generic GPT sounding reply. Thanks though at least I learned an acronym (no snark).

monsieurbanana
8 replies
1d9h

I can't blame you for thinking it was a GPT reply these days. Anyway I'm also not GGP but he's right, it is Hot Module Replacement.

You don't really need specific tools for HMR in clojure, it's kind of baked in the language thanks to:

- immutability (not impossible but hard to do HMR without it)

- REPL workflow

When developing on clojure, there's a long-lived clojure process and you continually send expressions to be evaluated there.

Let's say you start a server, you make a change to one of the functions that handle a route, just after making the change you will evaluate that function (from your editor) and the update will be "live" in the running server.

It's honestly hard to describe and it doesn't help that we call that "repl-based workflow" when in fact we are not using the REPL directly, but rather through plugins available for various editors. People might think it's the same as using nodejs or python repl, when it's nothing like that. If you're interested, you could check "Parens of the Death" or look for "Clojure CALVA" (VSCode's clojure plugin) videos on youtube.

https://www.parens-of-the-dead.com/s2e1.html

Edit: It is also not limited to development. Nothing prevents you from hooking into the clojure process in your prod server from your editor, editing a function and evaluating it.

You just monkey-patched a live server to fix a time-critical bug with roughly the same workflow as editing your code locally. Or brought production down, 50/50, but what's life without some risks eh?

38529977thrw
4 replies
1d8h

Just for the record, in Java (JVM) this is done (routinely circa first 'bean containers' of ancient history) via interfaces and class loaders. Using Java I can have n distinct versions of a class each implementing the same interface in the same process as long it is the interface reference that is passed around.

immutability

Make a case for immutability being so critical to HMR. How does an immutable data allow for swapping of code.

groestl
1 replies
1d7h

AFAIK, Java's native HMR is not built on dynamic classloaders, but using a separate technology built into the JVMs debugger support, called HotSwap. You can only replace (certain) function bodies with it. To get around these restrictions, more elaborate HMR can be achieved with classloader magic, such as OSGi, but you subscribe to a world of ClassCastException-pain with it.

kaba0
0 replies
8h41m

It is still a very good system, and you can get quite a far away with only method-swaps, I love to use them in case of Spring apps, where changing a backend controller and doing another api call is a very fast REPL cycle basically.

But objects don’t give themselves very easily to hot reloading, there are a bunch of edge cases that are not well-defined in a semi-swapped state (e.g. this new field always gets a value in the constructor, but was no just added. Should you rerun the constructor, or have it be in an inconsistent state?)

Immutable/FP concepts are a much better fit, especially if the building blocks are as small as they are in case of Clojure.

everforward
1 replies
1d1h

My limited understanding of Clojure's HMR is that there aren't n distinct versions of a class/function. It's actually removing the old ones from memory and replacing them.

Also, it is worth noting that everything is immutable, not just data. Functions/protocols/records/etc are also immutable.

Functions/classes being immutable makes it much, much easier to reason about the dependencies of that function/class. E.g. check out this Python code:

    import someotherpackage
    import threading
    import random

    class MyLogger:
        def __init__(self, name, path):
            self.path = path
            self.name = name
        def log(self, message):
            with open(self.path, 'w') as fd:
                fd.write(message)

    my_logger = MyLogger("init-logger", "init.log")
    someotherpackage.logger = my_logger
    someotherpackage.logger.error = print

    print = someotherpackage.logger.error
    my_logger.thingy = "thingy"

    def change_logger():
        def actually_do_change():
            time.sleep(random.randint(1, 100000))
            new_logger = MyLogger("main-logger", "main.log")
            someotherpackage.logger = new_logger
            someotherpackage.logger.error = my_logger.error
        time.sleep(random.randint(1, 10000))
        threading.Thread(target=actually_do_change).start()

    time.sleep(random.randint(1, 10))
    threading.Thread(target=change_logger).start()
Now imagine that this isn't our `main`, this is some library that's a transitive dependency of something we're importing.

Trying to hot-swap this would be awful. The big issues related to mutations are that a) there's no indication in someotherpackage that it's behavior depends on this package, and b) because mutations are allowed, the order that things are reloaded in matters, and c) some mutations are time-bounded.

If I change MyLogger and the VM/interpreter wants to hot-swap MyLogger, it has to recognize that it can't just re-load the instances of MyLogger with the same state. It has to re-load those instances, then re-mutate them, then re-mutate someotherpackage, and finally re-mutate the print function. It might also need to do the stuff that's delayed by that thread. Maybe. Depends on a couple of random integers that probably got GCed a while ago.

If you want to add an extreme layer of annoying, consider that this package could be a late import and only happens if a plugin is enabled, so the earlier code might rely on `print` actually being `print` or on someotherpackage.logger behavior that changes over time.

None of that applies in an immutable world. Nothing can mutate the someotherpackage package nor the `print` function, ergo nothing can depend on an earlier or different version of them. Dependencies are easy to track because the only way to introduce them is to import/directly reference them, or have them passed as parameters.

I don't know that it even needs any dependency tracking, though (provided they're using a pointer to a pointer, or maybe something smarter than that I can't think of). Immutability means that a) everything can be passed as pointers safely, b) those pointers can't be modified by the code, and c) there is never a reason to have more than one copy of a pointer to the bytecode for a function.

Each function gets a pointer to its bytecode, let's call that p1. Every reference to that function is a pointer to p1, which we'll call p2.

When you want to hot-swap code the language pauses the VM/interpreter, recompiles any function with a diff (easy with an AST), puts the new functions in memory somewhere new, changes p1 to point to the new bytecode, then marks the old bytecode for GC.

If you tried something that simple in Python for the code I wrote above, it would explode. It's an infinitely harder problem.

Using Java I can have n distinct versions of a class each implementing the same interface in the same process as long it is the interface reference that is passed around.

It's been a while since I worked with those app containers, but from my recollection it shares virtually nothing with hot reloading. E.g. is state preserved between those? If I'm caching stuff in RAM, and I change that class out, does it keep the cache? HMR does, as I recall.

My understanding is that app containers basically just run N versions of your app/class and allow you to choose which one you route execution to.

App containers are more similar to blue-green deployments with a load balancer. Each instance is totally separate, and the load balancer lets you choose which one you route to. App containers just do that process inside the JVM.

That's not bad, but it's also not as good as being able to repeatedly tweak a function without ever clearing caches or re-connect to downstreams or etc.

roenxi
0 replies
11h16m

My limited understanding of Clojure's HMR is that there aren't n distinct versions of a class/function. It's actually removing the old ones from memory and replacing them.

It is almost both. Clojure creates n distinct versions of the function (which are in fact objects and subject to garbage collection). The symbol of the function is rebound (technically this language is wrong) to point to the most recent one. Then, usually, the Java garbage collector deletes the old object.

So it is possible (likely under some code styles) that old versions of a function can hang around in a REPL environment if they ended up embedded in a data structure. However, if you make the call by resolving the function's symbol then that will reliably call the most recently def-ed version.

For example:

(defn poor-style [] 1)

(def object {:fn poor-style :call (fn [] (poor-style))})

(defn poor-style [] 2)

(prn ((:fn object))) ; => 1, calls the old embedded function

(prn ((:call object))) ; => 2, calls a function that resolves the symbol, finds the new version.

(prn (poor-style)) ;=> 2, just call the new function

beepbooptheory
2 replies
1d6h

I have a lot more experience with common lisp, but currently doing AOC with Clojure right now to try and position myself better for my dream job where I can work with any kind of lisp. Thought I could maybe ask a quick question I have been having trouble answering? I find that with emacs/cider any evaluation is blocking. I am a little spoiled on SLIME/SLY, where it has the nice baked-in feature that any evaluation you do interactively becomes its own process and you can carry on evaluating other things if the process is long running. Is there any way I can replicate this in clojure? Is it just a matter of using `future` explicitly? Or is there something I am missing with cider?

monsieurbanana
0 replies
11h42m

I don't think you are missing anything, I am not aware of anything that would automatically launch new threads when evaluating something.

That does seem like an interesting, although in practice I rarely have to evaluate something that would take more than a a couple of seconds, so manually wrapping it in a future seems acceptable.

On a semi-related note, sometimes I mistakenly evaluate something that will take a long time, or there's a bug and I'm stuck in an infinite loop. CIDER has a command to interrupt the on-going evaluation but I'm my experience it only works about half the time. In those cases having it wrapped in a future would certainly be helpful.

Volundr
0 replies
1d4h

If you want a long running calculation to not block the REPL you'd need to use a future, or some other method to get it off the REPL thread.

I don't remember what your describing being a feature of SLIME, but it's been 10+ years so I may just not remember.

eddtries
2 replies
1d10h

Fwiw it's a quote from https://webpack.js.org/concepts/hot-module-replacement/, what else would you like to know about HMR?

PaulHoule
1 replies
1d8h

It's actually the answer ChatGPT should give, with the citation to back up the quote.

weikju
0 replies
21h41m

if only.... lol

mattsan
1 replies
1d9h

Have you ever used a dictionary?

mattsan
0 replies
4h3m

Apologies for the tone. Was really tired when posted.

weikju
0 replies
21h40m

Thanks all for the various answers. I guess, coming from common lisp where it's easy to add a new dependency (via asdf / quicklisp) to a running REPL, it's not so easy (yet? until 1.12?) with Clojure. Various utils have been created over the years, so was just wondering if there was something more to the picture that fell under this new term I learned, "HMR"..

travisjungroth
0 replies
1d4h

If you want better answers, ask better questions.

packetlost
13 replies
1d6h

What do you use for an editor/IDE? I'd love to write more Clojure, but the tooling story is a bit confusing

Volundr
11 replies
1d4h

In my case, Emacs+clojure-mode+cider. I'm not sure I recommend learning Emacs at the same time as Clojure though. I usually recommend new users look to Cursive(for IntelliJ) or Calva (vscode) depending on what they are most comfortable with.

packetlost
10 replies
1d4h

I'm a vim user and there's no way in hell I'm switching to emacs, even with EVIL mode. I'll check out Calva

fiddlerwoaroof
4 replies
1d4h

I forget what the current recommendation for vim is, but back when I still wrote a lot of Clojure, people used vim-fireplace. I think there is a newer one for NeoVim, though

epiccoleman
2 replies
1d3h

Almost ten years ago, I gave a lightning talk about Overtone (Clojure mappings for SuperCollider). I used "vim-fireplace" to connect to cider-nrepl and had a pretty good experience being able to quickly eval sexps in Vim.

I'm sure this is wildly outdated by now - I'd be shocked if there wasn't something halfway decent in the NeoVim ecosystem.

I will say, though, that during a brief flirtation with Spacemacs, I absolutely loved the Clojure integration - emacs just feels like it's more "at home" with lisps. Some of the most satisfying programming experiences I've had, honestly.

cutler
1 replies
22h53m

As a Vim user I agree. Spacemacs is an excellent Clojure dev environment.

epiccoleman
0 replies
2h52m

The spacemacs / emacs feature I miss constantly in Vim / VsCode is "slurp/barf" - which is amazing useful in plenty of contexts that aren't s-exps.

fiddlerwoaroof
0 replies
1d4h

I too resisted emacs for years while writing Common Lisp in vim, but eventually I ported my vim config line by line to the emacs lisp implementation of the vi standard

manishsharan
1 replies
1d4h

Checkout Cursive on Intellij. The $100 on Cursive is the best $100 I ever spent on a tool.

wiseowise
0 replies
1d2h

Author of Cursive is on Hn also.

bokchoi
1 replies
1d3h

Check out Conjure for nvim: https://github.com/Olical/conjure/

packetlost
0 replies
1d3h

I'm currently a Conjure (and vim-sexp) user, but was wondering if there was something Clojure specific that didn't also have annoying tabbing defaults

Volundr
0 replies
1d4h

Hopefully someone will chime in with their recommended setup for vim. I think there is pretty mature tooling available, but I'm not immediately familiar with it, so my advice there would be no better than Google.

Calva is a solid choice if you don't mind switching editors.

drikerf
0 replies
1d1h

Emacs + CIDER and paredit. Works great!

pif
6 replies
1d7h

Out of topic, sorry!

Backwards compatibility is hugely important

Why is this always considered a Good Thing (TM), apart when applied to C and C++?

Why is it OK when languages and systems are bent backwards in order to avoid breaking stuff that works, but the two languages that together support a huge part of the running software are expected to up and break ties with their glorious past and present?

aphyr
2 replies
1d7h

There's a happy medium, I think, and I also imagine that depends on scope of change and the kind of applications involved. Speaking personally, I'm OK rewriting code, say, every decade. In the Ruby world, things moved so fast that my code would rot every six months. Now that I've been maintaining the same software for 20 years I'm really starting to care about that. ;-)

pif
1 replies
1d7h

I'm OK rewriting code, say, every decade

If it is your code, or your company's code, why not? But think about dependencies!

aphyr
0 replies
1d6h

I maintain dependencies for a lot of people--nowhere near operating systems or stdlibs, but my OSS gets fairly broad use--and I try very hard not to change their APIs too much! It's a real concern.

yogsototh
0 replies
1d5h

Let me talk about my experience. I was (yes was) mostly an Haskeller. I loved using Haskell, and even a long time ago (in 2007 I think) when I learned it, the book to learn the basic of the language had snipped that no longer worked.

But I still loved, it. And it changed every year, and I was even part of the proponent of the changes.

But after a while, you realise that your old project stop working. That if you want to use a new library, you also need to update many dependencies. But each one has breaking changes, so you start updating your code.

Mainly, if you wrote some code, and forget about it, it will no longer work or will be very hard to use with newer libraries. After a while this became very tedious.

In Clojure, the code I wrote 10 years ago is still working perfectly today. The application still launches.

If a bug is discovered and I need to fix it, for example, by upgrading or adding a new lib, I am not afraid to loose hours finding an fixing my existing code that suddenly become deprecated.

So yes, stability is VERY important for me now.

And last but not least, Rich Hickey talked about it in this talk and make a lot of very great points:

https://piped.video/watch?v=oyLBGkS5ICk

wiseowise
0 replies
1d2h

First one is a security nightmare, second one probably has more features and intricacies than other top three (by popularity) programming languages combined.

rhaen
0 replies
1d6h

I'll also note a lot of objections to the way C++ does backwards compatibility is their adherence to an ABI which they refuse to break, but also refuse to say they'll never break.

Many of the problem that can't be fixed for backward compatibility reasons are because they'd break the ABI, not new code. I think that's very different from other language's policy, which, from the ones I'm more familiar with, is about building old and new code together, rather than linking old and new code together.

It makes for a much more restrictive, but also ambiguous and not guaranteed, set of requirements on any change.

alberth
2 replies
1d9h

What % of the overall benefits would you attribute to the JVM vs. Clojure (the language)?

jwr
0 replies
12h22m

I barely even care about the JVM. I'm happy because it is an impressive feat of engineering, representing tens of years of huge investment into development, compiling my code to levels of performance that poorer VMs cannot reach. But JVM is just one of the environments where my code runs (the other is the JavaScript VM in browsers).

iLemming
0 replies
1h32m

Clojure is not only on JVM. There's Clojurescript. There's ClojureDart. There's ClojureCLR. There a bunch of experimental things like ClojureRS (Rust) and Clojerl (for Erlang). You can "talk to" Python and R from Clojure. You can use Fennel if you need to deal with Lua (It's not Clojure, but it's very Clojure-like lang). The benefits are there, for any platform you choose.

whalesalad
28 replies
1d15h

Clojure’s biggest issue is definitely the ecosystem. As far as the bell curve goes for accessible and usable tooling it’s usually either on the far left end and terrible but easily accessible (but doesn’t follow the functional paradigms well) or it’s on the far right end -extremely esoteric, LOTR quote in the readme, everything is data to the n-th degree, only accessible to dragons who can write emacs plugins blindfolded and upside down. The language itself is amazing, and Java interop is much better than Aphyr makes it sound. If there was a rails or Django for Clojure for instance (or better yet, a Phoenix) it would be an easier sell and methinks more widely adopted. The juice needs to be worth the squeeze - particularly when you’re convincing a team to jump from Algol style imperative languages to lisp. But at this time you need to reinvent the wheel on virtually any project, aside from fundamentals like http routing (which is also a rough scene) But the fact that literally any maven package can be used is tremendous. Plus it compiles to a jar, so anywhere a jar will run - so will clojure.

silcoon
18 replies
1d15h

You should definitely check out Calva, a wonderful plugin for VSCode which bring the REPL and much more (live documentation) for an easy way to use Clojure. I'm using it everyday at work.

https://calva.io

civilitty
17 replies
1d14h

Situation: There are three competing Clojure development environments.

Three!? Ridiculous! We need to develop one universal Clojure environment that covers everyone's use cases.

Situation: There are fourteen competing Clojure development environments.

sokoloff
8 replies
1d11h

The only very commonly used language that I can think of with a single dev environment is Excel.

Every other one (c, c++, Java, various .Net, python, Ruby, and yes, Clojure) has a variety of editors and dev tool sets in use.

Is choice of tooling somehow bad?

freedomben
5 replies
1d9h

I would disagree that Java has a variety of editors and dev tool sets in use. Of course there are occasional people that branch outside of jet brains, but they are a tiny minority. Most Java projects I have worked on are dependent on IntelliJ.

Given this end the fact that many clojure people come from Java, it does not surprise me at all to see this attitude.

sokoloff
4 replies
1d9h

Interesting. I would have expected "Java devs not using Jet Brains" to vastly outnumber those using it, rather than being a tiny minority.

Trying to estimate their sales, it seems like JetBrains is around $400M/yr in revenue. Looking at their pricing, it seems like $400/user/yr is a fair estimate, which suggests around 1 million paying users for JetBrains. Oracle claims 10M or more Java devs (undoubtedly many of those are dabblers in Java rather than 2000 hours/year users, but JetBrains sells to a lot of non-Java devs as well).

xcv123
1 replies
1d3h

Jetbrains took the #1 spot from Eclipse many years ago. The free community edition is fully capable.

owl57
0 replies
21h54m

And another free IDEA edition — Android Studio — might be even more popular.

freedomben
1 replies
1d5h

Interesting indeed! My sample definitely skews toward the small company/startup field, although I've worked with some big banks too a those devs were all using IntelliJ.

ecshafer
0 replies
1d3h

I worked for a finance company that everyone was using Eclipse, and it was almost impossible to break out of eclipse (except maybe for a new application) because there were custom Eclipse plugins written to even get some applications to compile locally.

owl57
1 replies
1d11h

> The only very commonly used language that I can think of with a single dev environment is Excel

Not sure if this is still the case, but last time I tried to write something in Kotlin it didn't have decent support in any editor outside of JetBrains.

Capricorn2481
0 replies
1d7h

And that's been a negative for Kotlin for a while now

pharmakom
3 replies
1d14h

This comment has been repeated many times. I think there is even an XKCD.

Anyway, it’s not very helpful. If the 3 existing solutions are all deficient in some way then a new standard may be a good idea. And besides that, we may not realize that what we have is deficient until someone explores the space a bit.

Edit: Please forgive my tone, I’m weary from seeing this repeated so many times and you have unfairly taken the brunt of that!

civilitty
1 replies
1d14h

woosh

They've been exploring going on 16 years. It's time to stop exploring and start exploiting.

pharmakom
0 replies
1d14h

It takes decades for programming tools to settle. Consider that most working developers today are using languages that wouldn’t have impressed a PL researcher in the 80s.

pzs
0 replies
1d14h

Indeed there is an XKCD on this: https://xkcd.com/927/

BoiledCabbage
2 replies
1d13h

Situation: There are three competing Clojure development environments.

Three!? Ridiculous! We need to develop one universal Clojure environment that covers everyone's use cases.

Wait, your argument is that it's a bad thing that users have choices of dev env for their language? How in the world is that a negative criteria?

Would JS be better if it could only be written in a single dev env?

I'm assuming I'm missing something here as otherwise this makes no sense to me.

acupofnope
1 replies
1d10h

It's a joke referencing this XKCD comic: https://xkcd.com/927/

nerdponx
0 replies
1d6h

Right, but the joke is misplaced here. nobody is talking about developing standards.

silcoon
0 replies
1d13h

Where you saw that they are competing?

raspasov
2 replies
1d13h

"either on the far left end and terrible but easily accessible"

Examples? I've been using Clojure since ~2013 and I can't think of "terrible + easily accessible" tooling.

Most of the popular libraries are excellent. They are head and shoulders above in terms of stability/backwards compatibility/simplicity than most other ecosystems that I'm aware of.

whalesalad
1 replies
1d7h
raspasov
0 replies
23h3m

I recall using korma way back I and I don’t recall it being terrible but I would say https://github.com/seancorfield/honeysql has very much superseded it by this point… (but I can see how that might not be obviously clear if one is to look at superficial metrics like GitHub stars for example…)

travisjungroth
1 replies
1d14h

This just seems like a random Clojure comment rather than anything to do with Jepsen or the article.

user3939382
0 replies
1d13h

So

DeathArrow
1 replies
1d13h

I think the biggest issue is it being a rather niche language and not having enough adoption. That would make it harder to hire and more expensive.

But if you can do it with a small team, it can work. Or you can do it in Clojure and rewrite at a later time.

sokoloff
0 replies
1d11h

Or why not do it in Clojure and teach competent devs Clojure? (I think this is the Nubank approach, undoubtedly among others.)

TacticalCoder
0 replies
1d8h

Plus it compiles to a jar, so anywhere a jar will run - so will clojure.

I'd add, for those who don't know Clojure, that as /u/jwr commented in this thread: Clojure also runs anywhere there's a browser, thanks to ClojureScript. And thanks to source file that can be shared between both Clojure and ClojureScript (.cljc source files), you can have the very same Clojure code and run it both on the front-end and on the back-end.

So not only the JVM "reaches" (as Rich Hickey says), but JavaScript reaches too.

... only accessible to dragons who can write emacs plugins blindfolded and upside down

I know you're joking but although I use Emacs since forever and can configure it to my liking and can eventually come up with some elisp code doing what I need, I'm not good at elisp by any means.

But I'll admit that learning both Emacs + elisp + Clojure at the same time for someone who's never learned any Lisp dialect is going to be hard (and a very hard sell).

Now there's a Clojure LSP server (which should help) and it works not only from Emacs but also from other IDEs. It's not mandatory to use Emacs for Clojure development even if many (most?) Clojure devs do use Emacs.

LouDNL
0 replies
1d14h

You obviously dont code in Clojure for a loving. Clojure's eco system and community is well maintained. Clojure comes in multiple flavors where Clojure and Clojurescript are 2 mainly used. Ofcourse there is also Babashka, Scittle etc., but all derive from clj or cljs. As far as development tooling it all depends on the platform you code on and what ide suits your needs. Emacs, Neovim, VScode, IntelliJ for example have different tooling. Build systems differ and suits ones needs, for clj deps.edn is the current lost used followed by leiningen. For cljs there is shadowcljs and many more. I agree that as new comer this can be quite overwhelming, but the community is great and always helpful. I program for a living and have done so in multiple languages, Clojure is by far my most favorite.

qsort
27 replies
1d16h

Sort of a meta comment -- the article starts saying that people keep asking "why Clojure".

Why such an emphasis on what language stuff is written in? Why does it even matter? Sounds like it was a good engineering choice given the constraints of the problem, get over yourself ffs.

simongray
9 replies
1d16h

It's probably because the author keeps getting comments from other developers about his choice of language and he's tired of explaining why.

There was a HN submission yesterday about another project of his (also in Clojure) and many of the comments were "Why is this made in Clojure?".

Unfortunately, a large segment of developers think there is no value in venturing beyond C-style languages.

Another dimension in recent years is the static type checking cult, whose adherents must warn everyone else in the cult whenever they spot any kind of dynamically typed programming language, e.g. causing a separate thread about the lack of static type checking in any comment section about a Clojure project.

andai
4 replies
1d15h

I came from C#/Java to JavaScript and it was extremely painful. I didn't care that much about typing until I experienced a dozen types of error that should have been impossible. (Shouldn't even compile, let alone silently poison your data at runtime...)

For years I thought it was a dynamic/static issue. Then I used Python (without type hints), and almost all the JS errors were impossible in Python too! Turns out it's about weak/strong typing instead.

Put simply: when given something that is an obvious error, throw an error. This sounds obvious, but given an obvious error, in many cases JavaScript remains silent and passes garbage data to the rest of the program.

I think if JS added a "use strong", it would remove 80-90% of the need for TypeScript.

yett
2 replies
1d14h
andai
1 replies
1d13h

Yes! Exactly. However:

    "use strict";

    let undef;
    let str1 = "hello";
    let str2 = str1 + undef;
    console.log(str2); // -> "helloundefined"
Whereas I propose:

    "use sane";

    let undef;
    let str1 = "hello";
    let str2 = str1 + undef; // -> Uncaught TypeError: Cannot append string and undefined
    console.log(str2); // (code never reaches this point)
(I would also settle for "Uncaught ReferenceError: Attempted use of `undef` before initialization.")

cmrdporcupine
0 replies
1d9h

Yeah, JavaScript has a pile of terrible type coercions, implicit conversions and misuse/reuse of operators. It was a bad 90s/00s trend to do this kind of thing; re-use + for string append, or coerce things into strings for convenience. Python and Ruby have their own gotchas, though not as bad.

You might enjoy this classic comedy routine: https://www.destroyallsoftware.com/talks/wat

dgb23
0 replies
1d9h

With VSCode, you can use "// @ts-check", which essentially turns on a linter that assumes JS is strongly typed and respects any provided TS definitions and jsdoc comments. It's not the same as actual strong typing (like Python/Clojure), as it obviously doesn't affect the runtime, but it really helps.

Scarbutt
1 replies
1d15h

The blog post does mentioned the lack of a static type system as a drawback. Although it sounds that what he really wants (cause of his prototyping remarks) is gradual typing similar to typescript.

aphyr
0 replies
1d7h

Clojure has a gradual type system--core.typed--that I've used extensively. It's... it's very cool, but for my uses it seems to be more trouble than it's worth. I keep coming back to it every few years, though.

tosh
0 replies
1d11h

There was a HN submission yesterday about another project of his (also in Clojure) and many of the comments were "Why is this made in Clojure?".

https://news.ycombinator.com/item?id=38525968

eyelidlessness
0 replies
1d16h

Another dimension in recent years is the static type checking cult, whose adherents must warn everyone else in the cult whenever they spot any kind of dynamically typed programming language

Shun the nonbeliever!

NateEag
7 replies
1d16h

Why such an emphasis on what language stuff is written in? Why does it even matter?

polyglot programmers are the exception, not the rule, and even for people who have mastered >5 languages, there's still no one who's mastered the top 50, let alone all the languages out there.

Language impacts both codebase contributor pool size and codebase reviewer pool size. In large, conservative orgs, the latter can matter a lot - good luck getting an insurance megaco's security review board to approve the cool new tool you found that's written in Eiffel - none of them know it, so none of them can review it intelligently.

Language diversity is a good thing, IMO, and there are many different factors impacting what you choose, and that makes your choice more important, not less.

kaba0
5 replies
1d13h

Taking up a new language that shares some roots with one you are a master at is/should be trivial to any good programmer, and can be done in weeks/a few months tops.

cmrdporcupine
2 replies
1d9h

Lisp-ish languages are maybe an exception to this, though. And not because of the (strange to many) syntax (or lack of it).

The culture of how to write programs in Lisp or Schemes is quite different, very much focused on using recursion, macros, code as data, etc.

It can be hard to read through a Lisp program if you're not used to it.

kaba0
1 replies
1d9h

Similar roots handle that part. Though I don't think it's all that different from FP-languages.

cmrdporcupine
0 replies
1d7h

Similar roots

Yeah, fair, I re-read that part after I made my comment and you're right. Though it's a bit weird with Clojure with it being a JVM-based language, it's got this hybrid world which is both Lispy and Java-ish.

NateEag
1 replies
21h48m

Reaching basic literacy - sure.

Reaching the kind of mastery where you see extremely subtle mistakes instantly - well, no, I think that really does take years to reach, even if you're an expert in a similar language.

I've picked up TypeScript for $DAYJOB in the past year, and there are definitely still deep aspects of the type system I only half-understand.

Granted, my decades of JS experience didn't really prepare me for static type systems, nor did the years of Python's strong-but-dynamic typing.

Still, I'm reminded of the quote "A language that doesn't change how you think about programming isn't worth knowing."

Most languages have some corners that are genuinely unlike most other languages, and without knowing those bits you don't count as a master, IMO.

kaba0
0 replies
15h24m

"A language that doesn't change how you think about programming isn't worth knowing

I don’t think it’s a good quote with such a bold claim, but I agree with the core of it: you should try to learn languages that have different paradigms. There is still value in knowing two quite similar languages.

sokoloff
0 replies
1d11h

megaco's security review board to approve the cool new tool you found that's written in Eiffel - none of them know it, so none of them can review it intelligently.

This suggests the frequent existence of a megacorp’s security review board that could intelligently review a tool written in a more mainstream language. That doesn’t exactly match my experience.

Forward unreviewed log output from a scanner they bought and ask you to respond with whether that’s a problem? Yes, they can do that.

p-e-w
1 replies
1d16h

Why such an emphasis on what language stuff is written in?

Because it's by far the most important decision made during the creation of any software project, and to a large extent, it sets the boundaries of what the software will ever be able to do.

Experience also teaches that language choice strongly correlates with overall project quality. When I see a CLI project written in JavaScript (Node), I know that it's probably not worth my time to take a closer look. With Rust, Clojure, Haskell, F# etc, the opposite is the case. This is a cultural effect, rather than a technological one, but that doesn't make it any less real.

kaba0
0 replies
1d13h

While the cultural effect might indeed be quite important, I really don't agree that it is by far the most important decision.

E.g. from a performance perspective, the design of you program is orders of magnitude more important than the language itself. Also, not every platform is as limiting - you could for example start a clojure project, and extend it in java without any fuss.

lemper
1 replies
1d16h

because it's not rust, bro! if it's rust, people will accept it and move along.

aphyr should've written it in ada, though.

sitkack
0 replies
1d15h

Erlang

tasuki
0 replies
1d15h

I don't understand your comment.

Of course people ask "why clojure", we're curious!

Why such an emphasis on what language stuff is written in? Why does it even matter?

Because different languages (and especially different paradigms) are different. The choice of language affects the project's future prospects, productivity, expressivity, maintainability, developer satisfaction, ease of finding developers, and a million other things.

justin66
0 replies
1d9h

Why such an emphasis on what language stuff is written in?

People are interested in the decisions that informed the engineering of a tool they respect.

People are interested in reading the code but aren’t going to learn something as goofy as clojure (sorry) to do it, and this causes them to whinge.

Some combination of those two. I’m sure the second one is purely hypothetical since no one ever whinges on the internet.

cpitman
0 replies
1d15h
contrarian1234
0 replies
1d15h

Bc the whole "data-drive development" is fundamentally a pretty different programming paradigm. You can do similar things piece wise or with a library in other languages but the Clojure language and all your dependencies are structured around it in a cohesive way

You wouldn't be able to translate Clojure to another language line by line unless you brought in some framework or library

Uehreka
0 replies
1d16h

Why such an emphasis on what language stuff is written in?

Because people are interested in the way that languages affect your ability to think and solve problems? Because different languages have different tradeoffs and the tradeoffs involved in less used languages are often underexplored? Jeez dog, ease up.

rtpg
18 replies
1d14h

Last time I spent a couple klines trying to write some clojure (+ clojurescript), the big thing was that most of the tooling had some really nasty error states. Python has some of this (ctrl+C out of most Python programs and get that stack trace with a bunch of inscrutible stuff), but given I was just starting out it was tough for me to move forwards.

I do think the language + ecosystem has so many innovative and interesting ideas built in. Definitely worth at least reading the history of Clojure paper and internalizing most of the lessons.

This is going to be anathema to everyone but I think there's a nice little ecosystem space for a scripting language that is as serious about performance and code ergonomics as Clojure... but without the parens[0] (and I could do without the nil-punning, but at least there's a bit of a foundation there)

[0]: yes I know that (f x y) is nicer than f(x, y). My problem is more when it comes to things like let forms introducing indentation (I shouldn't have to pay an indent + paren cost when trying to give a value a name!) and other things that just feel like line noise when coming from a language like Python, that just uses the indentation I'm going to put in my code anyways as information

chii
14 replies
1d14h

let forms introducing indentation

this is a symptom of using let forms too copiously, rather than using a functional style, where there's very few requirements to attach names to intermediate arguments being passed between functions.

Often, you would use threading macros (the -> and ->> stuff) to implicitly pass arguments from one function to the next, or define transformations which are themselves just pure functions (so you would define it at the top level).

It's understandable that some interop require a lot of let forms - mainly those that mutate an argument in a chain of procedures (looking at you, java jwt libraries...you know who you are).

skrebbel
8 replies
1d14h

Well-named intermediary variables often help tremendously with readability though. A language discouraging their use seems like the wrong trade-off decision to me.

chii
4 replies
1d13h

that's because you have assumed that the code would _need_ such an intermediate variable, which is often the case in imperative code.

I argue that if your code _does not need_ this intermediate variable, it makes the code even more readable as it removes more cognitive friction - having fewer things makes it easier to understand.

skrebbel
2 replies
1d9h

If you name your functions and transducers well, you tell the reader what’s being done to the data.

If you name your intermediate data structures well, you tell the reader what the data means.

Often, the transformations have generic names such as “unique” or “removeEmpty”, but the variables can get domain-specific names such as “validOrders” or “paidAccounts”.

I’m a big fan of designing code in a datastructure-first way. If you get the datastructures right, the code itself is often easy to write and easy to read. This matches FP languages really well (eg immutability), but it doesn’t match long fluent chains/pipes of maps, filters and reduces well at all.

bcrosby95
1 replies
1d6h

You can name anonymous functions so your transformations can still have a domain related name associated with it, even if you're using standard library ones.

skrebbel
0 replies
1d4h

If you’re making a local anonymous function that you call in one place, just so that a generic function gets a more domain specific name, then you might as well just assign the result to a named variable, right? It’s the same benefit with less indirection.

Barrin92
0 replies
1d11h

the code doesn't need anything, people reading it do. Names are useful because they make intent explicit. Point free programming in functional languages is often overused. Stuart wrote a good blog post about this a few years ago.

"All too often, however, I see the threading macros used in places where they make the code less clear. Early in Clojure’s public existence, there was a fad for “point-free” programming, a kind of code golf without any local variables. This is a fun exercise, but it does not lead to readable code, and I’m relieved that the enthusiasm for this style has worn off. What has remained is a tendency to use threading macros any place they can be made to fit. I think this is a mistake. Rather than making the code easier to read, it makes it harder, forcing the reader to mentally “unwind” the syntax to figure out what it does."

https://stuartsierra.com/2018/07/06/threading-with-style

xmcqdpt2
0 replies
1d9h

The problem with naming variables is that you then have to deal with their scopes in review. For example, the following Java

    Foo getSmallFoos() {
      return foo
              .stream()
              .filter(...)
              .collect(...)
    }
is a lot more readable IMO than if each individual steps were named, especially because reusing stream steps is a runtime failure. Not having names is a good way to enforce linearity in the absence of affine types.

thom
0 replies
1d11h

I think well-named functions or transducers are possibly better in many cases. I agree that long, complex pipelines of transformations aren’t good to stare at though.

BoiledCabbage
0 replies
1d13h

Yes those are frequently used in imperative languages, but they aren't really as needed in functional styles as functions end up being smaller and more powerful.

The best analogy I can give is if you've used a fluent interface in Java or C# ( like Linq). Take a look at the two larger examples at the bottom of the link. It actually would hurt readability to try to add names for each of those intermediate stages.

And since functions are usually smaller you end up frequently using functions as your readability helpers instead of defaulting to let bindings.

Fluent interface - https://java-design-patterns.com/patterns/fluentinterface/#e...

rtpg
3 replies
1d12h

(This is a rant, i get what you’re saying)

I just disagree about me being the issue here. Sometimes I want to make names. They are basically documentation. This is just so normal!

Like you can’t just say everything should be written in a point free style. There are times that is more clear and then times where having a name to a value makes subsequent code clearer. Especially given that Clojure has some of the world’s most anemic comment support (no real multiline comment support without futzing with forms? In this economy?)

I do agree threading helps by making things more pipeline-y at times though. I just feel like being punished for verbosity is annoying. The verbosity is punishing enough, just let me write it!

epgui
1 replies
1d10h

The hardest thing about learning clojure is unlearning other languages… It sounds like you’re just still going through that.

rtpg
0 replies
1d9h

I’ve programmed in various MLs, I know about the unlearning issue. I have stared into the depths of the monadic transformers, and nothing Clojure does is close to what I've seen on that front.

I just think that being able to give names is fine and good. I understand this is more of a challenge because of s-expressions, and I’m not going to entirely fight the idea that sexprs and clojure might be deeply coupled.

But one can dream of something nicer!

roenxi
0 replies
1d10h

I wouldn't say you were the issue. The issue here is Clojure will fight you until you code in the style that it likes. I lost the battle, but Clojure's style is better than my old style so I became a much better programmer. And of course, Clojure is the best language for programming in Clojure's style.

If you don't like threading macros, try a 1-let-per-function style. I've seen some very capable Clojure devs who are in the habit of making lots of small, neat functions. It can be a lot prettier than threading macros. It works in Clojure because making small mutations to large objects is cheap - that enables small functions that tweak data structures in a way that would be unsafe in other languages.

da39a3ee
0 replies
1d10h

It’s essential to use names in programming to modularize code and introduce different levels of abstraction. Smart people can drift towards a point-free style because they enjoy the feeling that they are capable of comprehending their code. It’s important to resist the introduction of such code and to help such people see that neither they in 6 months, nor their colleagues now, will be able to understand it.

fulafel
1 replies
1d12h

There's definitely some things you learn from mistakes when using a REPL workflow in Clojure or Python, where you debug for a bit and then realize that you just didn't reload or reinitialize something. I feel most people end up adding it to the set of things to sanity check when hitting "can't happen" situations, like "let's add a debug print here to make sure my code change is actually invoked". But that's just a backstop of course, you mainly use other practices in your REPL workflow to keep from hitting it.

rtpg
0 replies
1d11h

I was thinking more about tools like `lein` just barfing up garbage when I did something wrong. This stuff might be totally better nowadays though! It's been many years since I touched the language.

freedomben
0 replies
1d9h

Last time I spent a couple klines trying to write some clojure (+ clojurescript)

Killing me! My brain says this should be (+ clojure clojurescript)

scotty79
7 replies
1d10h

The error messages are terrible. I have no apologetics for this. ;-)

Could something be done about this? I tried to learn Clojure but I bumped into that fairly early on and gave up. Rust was way more approachable for me because good error messages form learning dialog between the learner and the language.

phyzome
4 replies
1d10h

It's sad to hear this is still an issue.

For those who don't know, Clojure's error messages... aren't. They take the form of a stack trace from the compiler's internals, and that whole stack trace just gets barfed out onto the terminal. There's an exception message but it sometimes just isn't that helpful.

I know there was an effort a while back to improve the situation, but last I saw (2019) I believe things were only marginally improved. It's a shame because other than that it's really quite a fantastic language.

smw
2 replies
1d7h

Isn't that true in most common languages? Python and Ruby and Java and Go (panics) will all spit out the stacktrace?

scotty79
0 replies
1d1h

Clojure spits out Java stack trace if you make syntax error. It's equivalent of Python barfing segfault when you put tab in the wrong place.

phyzome
0 replies
1d7h

This is about compile-time errors, not runtime errors.

aphyr
0 replies
1d6h

Tbh I think they've actually gotten worse in recent years. This thing where the compiler spits out a stacktrace to /tmp and makes you go and cat it? Absolutely awful.

And the move to spec, ugh, UGH. I have wasted so much of my life looking at spec errors for (e.g.) namespace declarations and being completely unable to tell what was wrong. I don't know how to fix this, unfortunately.

sometimesweenie
0 replies
1d8h

When I was learning Clojure, I just sorta dealt with it. Usually you are presented with a trace that contains a line number for the error which was enough. I’m not entirely sure what could be done about the error messages but with experience and more idiomatic usage it’s infrequent for me to get stuck on these (and when I do I just step through it in the debugger).

dgb23
0 replies
1d9h

Depends on the tooling you use and how you program.

An REPL that is integrated with your editor is key.

A good linter such as clj-kondo (comes with Calva on VSCode), helps a lot to avoid typing errors and a lot more.

Using a REPL middleware which visualizes your output such as portal is very nice.

One issue is when you get errors that involve anonymous closures. It might be harder to navigate those errors. But I don't know of a language that does this well. Clojure makes it very easy to divide and conquer though.

Another big one involves macros. Macros are powerful, but errors you get from them can be quite cryptic in some cases. Usually for common macros there are linter rules that you can use/find to avoid those.

Clojure itself has a symbiotic relationship with nils. However as soon as you interact with Java data structures you might run into exceptions here. It's something to be wary of.

Finally, spec and malli give you tools to adorn your code with little specifications to state your assumptions and guarantees. You can automatically run those while you're writing code to get sharp, human readable error messages.

So yes, Clojure error messages can be scary/confusing at times, even though it has gotten better. But there are a lot of things you can do, or have been done for you that really help here and sometimes go quite a bit beyond of what you'd expect from a more mainstream language.

38529977thrw
7 replies
1d9h

It would have been worthwhile for them to mention why not use Java. I mean, grep for Java and JVM and that short blog lights up! It was good that they did stress the (very) small team size for the product as well. This was an aesthetic choice and I wish he had stressed that.

Java remains an excellent choice for concurrent systems, with a commonly accepted static type system, and all "java.util.concurrent" and the rest of JVM are native and the language and tools do scale to even monstrous sized "teams".

aphyr
6 replies
1d7h

I've been a Java programmer for ~18 years! A nontrivial portion of Jepsen is written in Java too. :-)

Many of the points I touched on in the post are direct critiques of Java, but maybe I should be explicit. An obvious factor is size--when I've written the same project in both languages, Clojure usually winds up 5-10 times smaller. That's definitely improving as Java adds streams, lambda expressions, and so on, but it's still a good deal more verbose. That gets in the way of the kind of exploratory programming I do, especially at the REPL, and it's harder for me to read and maintain a giant codebase. I appreciate having fewer nouns, in general: Clojure emphasizes a uniform way of traversing and transforming data structures, and in Java every single class has its own way of representing its data, and many of those ways are bad.

The standard collections library is full of mutability and thread safety pitfalls. Printed representations of datatypes are verbose. It's not particularly good at dealing with highly polymorphic data structure transformation, and it's not a particularly good static type system either--a good part of my Java Brain (TM) is devoted to knowing the mechanisms of erasure, memory layout, and when unsafe casts are actually the right choice. Functions are sort of becoming first-class with method refs and functional interfaces, but it's nowhere near the convenience and flexibility of a Lisp-1. Accessing the compiler at runtime is a pain in the ass. No hygenic macro system makes things like custom iteration or compositional error handling a pain. There's no analogue to Clojure's protocols, which are a fantastic tool for "Hey, compiler, I want a monomorphic-cached type-dispatch polymorphic call site here for a type hierarchy I don't control". Etc etc.

There are things I love about Java. Automated refactoring is easier, and I generally like the level of IDE support. Nearly anything involving primitives, I drop to Java. Ditto, APIs that require annotations. Interface specification is more rigorous. Etc etc. That's why I write in both languages. But most of Jepsen is in Clojure for good reasons. :-)

Capricorn2481
4 replies
1d7h

Nearly anything involving primitives, I drop to Java

Can you describe what you mean by this? Does this just mean you're using native Java data types sometimes when speed is a concern? Is there an example somewhere in Jepsen?

aphyr
3 replies
1d6h

Speaking very loosely, primitives on the JVM are values which are represented directly in memory, instead of as pointers to objects on the heap. Clojure (again, very loosely) generally treats everything as a pointer to a heap object. There is no specialized equivalent for, say, a vector of shorts, or a map where values are floats. The compiler can emit specialized function signatures for... IIRC longs and doubles, but other types (e.g. byte, float) aren't directly accessible--they go through widening conversion. It's also easy for the compiler to quietly fail to recognize it can preserve primitives in some kinds of loops, so you wind up with what Java calls "autoboxing": wrapping a primitive in a corresponding Object type.

Here's a recent example of some code in a hot path inside Elle, one of Jepsen's safety checkers. It does a lot in primitives, using packed structures and bitmasks to avoid pointer chasing.

https://github.com/jepsen-io/elle/blob/main/src/elle/BFSPath...

There was actually a Clojure version of this earlier that got pretty close perf-wise, but I wound up dropping to Java for it instead:

https://github.com/jepsen-io/elle/blob/913cbff5ebb19ba850c0a...

Capricorn2481
2 replies
1d4h

How often is this necessary? I haven't been able to make an example of Java code performing faster than Clojure. I tried to make the java equivalent of this in Clojure

``` (defn sum-clojure [size] (reduce + 0 (range 0 size)))

(sum-clojure 100000000) ```

Despite the fact that Clojure primitives are boxed, a manually constructed long array that was summed together in Java was much slower. Why is that?

aphyr
1 replies
1d2h

Necessary depends on your use case! I spend a lot of time waiting on analyses, and the more operations I can test, the more bugs I can find. I probably invest more time in performance optimization than most people.

Regarding your specific example, uh, I don't know how you measured, but that feels... off. Here:

``` package scratch;

public class Sum { public static long sum(long[] longs) { long sum = 0; for (int i = 0; i < longs.length; i++) { sum += longs[i]; } return sum; } } ```

``` (require '[criterium.core :refer [quick-bench bench]]) (def longs (long-array (range 100000000))) (defn sum-clojure [size] (reduce + 0 (range 0 size))) (import 'scratch.Sum) ```

Summing an array of 10^8 longs like you suggested takes ~120 ms on my machine.

``` user=> (quick-bench (Sum/sum longs)) Evaluation count : 6 in 6 samples of 1 calls. Execution time mean : 119.804166 ms Execution time std-deviation : 9.124709 ms Execution time lower quantile : 115.154905 ms ( 2.5%) Execution time upper quantile : 135.597497 ms (97.5%) Overhead used : 20.384654 ns

Found 1 outliers in 6 samples (16.6667 %) low-severe 1 (16.6667 %) Variance from outliers : 15.4197 % Variance is moderately inflated by outliers ```

Your Clojure function, which sums a lazy range, takes ~4.7 seconds--about 40x slower.

``` user=> (quick-bench (sum-clojure 100000000)) Evaluation count : 6 in 6 samples of 1 calls. Execution time mean : 4.739575 sec Execution time std-deviation : 346.895187 ms Execution time lower quantile : 4.557732 sec ( 2.5%) Execution time upper quantile : 5.324739 sec (97.5%) Overhead used : 20.384654 ns

Found 1 outliers in 6 samples (16.6667 %) low-severe 1 (16.6667 %) Variance from outliers : 15.3163 % Variance is moderately inflated by outliers ```

An idiomatic Clojure reduction over the same array of longs, just so we're measuring apples to apples, takes about 12 seconds--about 100x slower.

``` user=> (reduce + longs) 4999999950000000 user=> (quick-bench (reduce + longs)) Evaluation count : 6 in 6 samples of 1 calls. Execution time mean : 12.037821 sec Execution time std-deviation : 433.661044 ms Execution time lower quantile : 11.790637 sec ( 2.5%) Execution time upper quantile : 12.779262 sec (97.5%) Overhead used : 20.384654 ns

Found 1 outliers in 6 samples (16.6667 %) low-severe 1 (16.6667 %) Variance from outliers : 13.8889 % Variance is moderately inflated by outliers ```

Incidentally, this is one of the reasons I wrote `loopr` (https://aphyr.com/posts/360-loopr-a-loop-reduction-macro-for...). One of the iteration tactics it can compile to is iteration over arrays. Nowhere near Java--I think it's probably still boxing a fair bit, and I'd need to disassemble it to see--but it's still 10x faster than the standard reduce here. ~10x slower than Java.

``` user=> (quick-bench (loopr [sum 0] [x ^"[J" longs :via :array] (recur (+ sum x)))) Evaluation count : 6 in 6 samples of 1 calls. Execution time mean : 1.259095 sec Execution time std-deviation : 3.876650 ms Execution time lower quantile : 1.256692 sec ( 2.5%) Execution time upper quantile : 1.265747 sec (97.5%) Overhead used : 20.321072 ns

Found 1 outliers in 6 samples (16.6667 %) low-severe 1 (16.6667 %) Variance from outliers : 13.8889 % Variance is moderately inflated by outliers ```

Capricorn2481
0 replies
1d2h

Edit: I wonder if there's a caching thing going on. I am having wildly different performance depending on what number I put in. Does criterium affect this by isolating something?

I read a little more about this before you replied, but it seems like we're doing something different or there's a JVM difference that is bigger than expected.

- The clojure function takes me less than a second.

- (reduce + (long-array 100000000)) also takes less than a second.

- From what I understand, the compiler may have a special rule around using reduce and + that uses unboxed numbers by default

- Isn't it a big "if" that you might have a big array of primitive longs in Clojure? I can see how if you've already gone through a whole range of numbers and unboxed them then the java function would be fast. But it seems like it'd be easier to use + (again, this is running a lot faster on my machine for whatever reason).

I'm new to all of this so just being academic and trying to figure out what's going on

38529977thrw
0 replies
1d6h

Thanks for the thoughtful reply. Always interested in your thoughts on software matters.

You know my professional issue with Clojure is that it attracts poseurs. It's that strange spot in PLT that to appreciate it requires sophistication and experience yet barrier to entry (contra say Haskell) is much much lower and does not require the same. So you can get bragging rights without being someone like you who actually understands the cost equations in toto in context of picking languages. This human factor coupled with the technical matter of Clojure not being ideally suited for large scale code / long running / typical IT fubar realities.

You mentioned macros. I can tell you about 'sacred macros' that must not be touched :) Clojure may have addressed software compatibility but it has a human resources compatibility issue.

koito17
6 replies
1d14h

In short:

- being on JVM means Jepsen can use JDBC drivers to interact with databases

- Clojure was designed to facilitate concurrent programming (via software transactional memory and persistent data structures)

- Clojure's interactivity allows quick prototyping

- Clojure's community has a strong emphasis on backwards compatibility

- macros allow a great deal of code reuse

- while there are some big drawbacks (niche language, no strong static typing, ...), it isnt an issue in practice since Jepsen is only worked on by 1-3 people at a time

I agree with most of these points, but it seems overly simplistic of an explanation. I really wanted to see what other languages were used for prototyping and why they were deemed insufficient. For instance, Haskell is mentioned but the most treatment it gets is that it was too "dogmatic" for writing Jepsen. There was no comparison on how easy it is to interact with databases from Haskell as opposed to Clojure. Moreover, Jepsen was prototyped "in a few different languages", but they are not compared to Clojure, let alone mentioned.

aphyr
3 replies
1d7h

DB vendors generally don't write Haskell clients, which put a serious damper on my "test lots of databases for a living" project. Love Haskell as a language, but it's just not a popular target, and I wasn't brave enough to go for C interop. As I touched on in the post, Haskell is also rigorous about side effects--I've found it difficult to, say, inject side-effectual performance monitoring into an otherwise pure piece of code so I can profile it. Maybe Haskell experts are better at this, or the story is better now.

There was a Ruby version of Jepsen too. That enjoyed better library support and language flexibility, but it suffered from terrible performance, a standard library built for mutability, and a janky thread model. There was, at the time, essentially no way to reliably time out a Ruby thread. That was a dealbreaker all in itself.

I wrote some of Jepsen in Erlang too, but abandoned the project quickly. It's got a really good concurrency story. A VM designed for concurrency and failure with bounded reduction queues--honestly this is something I miss in the JVM. I love Erlang as a language (and I think some of my Erlang might still be in prod!) but it also suffered from a lack of client support, not being well-suited for high-speed mutable state, and generally terrible string manipulation. There's a lot of strings in Jepsen. ;-)

travisgriggs
0 replies
1d5h

Came here to ask about Elixir, since the opener requirements of your post kind seem like a sales pitch for the things OTP people get excited about.

This last bit answers that though I think. Thanks.

koito17
0 replies
1d2h

So you tried Haskell, Ruby, and Erlang, and each had their own deal breakers (database clients, threading, controlling side effects, etc). This is all worth mentioning in the article IMO.

I love using Clojure, but I will admit that the article left me wanting an explanation just like the one you've provided here.

Thanks for taking the time to respond!

asa400
0 replies
15h56m

Just to add an additional anecdote/perspective for other folks curious about the Erlang mention: the lack of client support in Erlang/Elixir is real, unfortunately. Just an artifact of it being a smaller ecosystem, but it's improving all the time.

You can do mutable state manipulation in Erlang/Elixir through a few mechanisms - ETS tables is the main one - but it's not "high-speed" in the sense you find in C/Java/Rust/etc., where you want to bash on a slice of mutable, contiguous memory containing primitive (word sized/aligned) values. It's also a bit awkward, as ETS tables are more like having Redis inside your application than they are an actual collection type.

Regarding string manipulation, strings in Elixir are actually quite good. This is one of Erlang's sore spots that Elixir improved upon. I'd put Elixir's string manipulation facilities up against anything I've used, including Java and Clojure.

And +1 to the point about Erlang's concurrency story. If you have a highly concurrent problem, Erlang/Elixir really is best in class. It isn't a perfect technology by any means but it really is as good as everyone says it is.

travisjungroth
0 replies
1d13h

Based on the title and intro, I think part of the point of this post was to have something to link to for a “good enough” answer to this common question. I’m guessing he wrote this relatively quickly. It was a choice made over ten years ago. Trying to recreate the decision would likely be rather time consuming, prone to mistakes, and really only invite more discussion.

fulafel
0 replies
1d11h

Indeed the protoyping takeaways would be interesting. It's quite rare in real world apps to prototype a system in several languages to pick one. From the text, to me it seems more likely to me that there were several prototypes for other reasons, and they might not be functionally or architecturally equivalent enough for direct language comparisons.

amai
6 replies
1d3h

Being a Lisp dialect for the JVM Clojure has readability issues: https://tonsky.me/blog/readable-clojure/ That alone will make sure Clojure will always be a niche programming language and ensure job safety for the few ones who can read it. But nowadays Clojure brings no advantages anymore over Java. I wouldn't start any new project using Clojure.

j-pb
4 replies
1d3h

I'd consider your point more if you could actually write the name correctly.

amai
3 replies
1d2h

Fixed it. It seems the autocorrection on smartphones doesn’t like Clojure either.

j-pb
2 replies
1d1h

You're missing a/the crucial point about Clojure btw. It's not about the functional programming stuff, of which Java has added a lot in recent versions, but it's actually about the immutable/persistent data-structures, of which Java has added none afaik.

Value types are simply not the same thing. The structural sharing you get with Clojure, means that you can have immutable data that contains billions of entries. This allows you to solve really hard problems that are almost impossible to get right any other way, undo-redo, search algorithms that exploit lattices, idempotent network operations.

amai
1 replies
14h10m

"the core JDK package java.util.concurrent includes CopyOnWriteArrayList and CopyOnWriteArraySet which are persistent structures, implemented using copy-on-write techniques...Fully persistent collections are available in third-party libraries"

https://en.m.wikipedia.org/wiki/Persistent_data_structure#Ja...

So if you need that in Java you can simply use 3rd-party libraries like https://pcollections.org/ or https://github.com/GlenKPeterson/Paguro.

There is no need to completely change your programming language to Clojure and suffer from all that readability issues: https://softwareengineering.stackexchange.com/questions/1972...

j-pb
0 replies
12h41m

There's a huge difference in friction between using a language that _also_ has a library for persistent data-structures, vs a language that was designed _around_ immutable datastructures.

If you had brought up error messages in Clojure as your main concern, I'd think you'd have a point, but the "putting the paren before the function name confuses me" argument is as old as lisp and pretty moot imnsho.

I did Clojure full-time for years, and now do Rust rust full-time, and I can definitely say that Rusts syntax is wayyy more complex and hard to read, despite technically being in the C style syntax family.

    Great minds discuss architecture, Average minds discuss semantics, Small minds discuss syntax
It's also not like the example you posted is easier to read in e.g. javascript if you remove all indentation.

    (defn f [{x :x y :y z :z [a b c] :coll}] (print x " " y  " " z " " a " " b " " c)) 

    const f = ({x: x, y: y, z: z, coll: [a, b, c]}) => print(x, " ", y, " ", z, " ", a, " ", b, " ", c)

iLemming
0 replies
1h50m

Readability? Every programming language suffers from "not enough readability". There doesn't exist any language that is always clear and easy to reason about. Writing is fundamentally a method of communication that involves the use of symbols to convey ideas, thoughts, stories, information, or messages. You can mess it up in any language. Clojure, being a Lisp dialect, does not have readability issues. Au contraire, I believe it is far more readable than any other general use, popular language.

Once you grok the key features of any Lisp, you may find Lisps (especially Clojure) to be clear and direct, and here's why:

1. Uniformity: Lisp languages follow a consistent pattern. Functions and operators always come first, followed by operands.

2. Minimal syntax: The minimal syntax can also be seen as making the language cleaner and simpler, decreasing the chances of errors.

3. Expressiveness: Lisp allows you to accomplish more with less code, which leads to fewer lines of code and thus potentially easier maintenance and readability.

4. Macros and meta-programming: The ability to extend the language by defining new syntax or deriving new languages can lead to more readable and concise code. This one's pretty much up for debate, because as easily as you can improve the language, you can also make it much more confusing. You know what Spiderman's uncle says.

People using non-lispy languages often get excited about certain things, to some of them I can only react with raising eyebrows. I always have a Clojure repl around. Even when I'm not programming in Clojure. Like for example, if I'm testing REST endpoints and sending data back and forth, I configure it in such a way that it always converts the responses to EDN. EDN is far more concise and much more readable than JSON. With the connected REPL I can quickly `filter, map, reduce, group-by, sort, remove`, dedup, slice, dice, and serve the data any way I like. It will take you longer to go through jq's documentation to perform the same data manipulation with JSON. It's an extremely powerful way of dealing with data. With large quantities of data. Clojure is the only language that can turn any impossible-to-navigate-through data and make it precisely readable for me. Readability? Really? Spend some more time using Clojure, you may get surprised how insanely readable it is.

ARandomerDude
6 replies
1d6h

Clojure...[has] no (broadly-accepted, successful) static typing system.

I've used Clojure exclusively for 7 years and actively work on a Clojure + ClojureScript codebase of more than 500K LOC. The lack of a robust type system is my number one frustration. Many people point out that Clojure is expressive, lets you go fast, and generally just gets out of the way. I agree. It also lets you have some nasty runtime errors that can be hard to reproduce and debug.

If there were a broadly-accepted, successful static typing system Clojure would be amazing. As it is, I think it's pretty decent if you like Ruby or Python and want more power. At this point I'm wondering if Go is the way.

erichocean
5 replies
1d5h

You can integrate another JVM language like Kotlin into your Clojure project in places where you have measured that static typing would help.

munificent
2 replies
1d3h

Snarky response, but semi-serious too: I've measured my own productivity and software performance and have determined that static typing helps everywhere. I've yet to measure and find any places that dynamic typing would help.

(I say this as someone who has already spent the considerable time needed to understand static typing. For newer programmers or someone who doesn't program enough to make the investment worth it, I totally see the value proposition of dynamic types.)

erichocean
0 replies
23h9m

static typing helps everywhere

I think "Structure and Interpretation of Classical Mechanics" is a good example of the kind of software a dynamically-typed Lisp is superior at, beyond the usual suspects like SQL, R, Python, etc.

Here's an example of SICM in Clojure: https://github.com/mentat-collective/emmy

Go here to give it a try: https://nextjournal.com/try/samritchie/sicmutils

And here's a conference talk about it's implementation: https://www.youtube.com/watch?v=7PoajCqNKpg

---

More broadly, I think Rich Hickey (Clojure creator) makes a reasonable case that for information processing systems in particular, static typing has negative value: https://www.youtube.com/watch?v=2V1FtfBDsLU

That's also been my experience.

Scarbutt
0 replies
7h26m

In which programming languages did you measure?

joshlemer
1 replies
1d3h

How do people measure whether static typing would help or not in any particular section of code?

erichocean
0 replies
1d1h

How do people measure whether static typing would help or not in any particular section of code?

Whether or not you are getting bug reports in production that static typing would have caught prior to deployment *merely by adding types* to your existing functions/implementations, and where adding trivial-to-write dynamic type checks/assertions is too onerous.

Dynamic type assertions are nice because they progressively insert static typing (they are caught by unit test == compile time) into dynamic languages, without having the full overhead of a static type checker. As a bonus, the technique can be used for assertions that are still research areas for static typing.

Separately, there are a lot of way language-supported static typing can help by completing changing the nature of the solution to a problem—see Haskell for what that looks like.

In my experience though, if I'm going to go through the trouble of adopting static typing everywhere, I just bite the bullet and go to full model checking with something like Alloy or TLA+. Different developers have different experiences, it's very dependent on the kind of code you write.

contrarian1234
5 replies
1d16h

out of curiosity, does it use core.async or manifold for concurrency? (and what are the tradeoffs)

stefcoetzee
3 replies
1d10h

Seems like `core.async`: https://github.com/search?q=repo%3Ajepsen-io%2Fjepsen+core.a...

(As an aside: Missionary is a higher-level, more recent option in this area.)

contrarian1234
2 replies
1d7h

Do you have experience using it?

Missionary looks very cool, but I don't think anyone understands how to use it. Other than Electric/Rama (from the same people) I haven't been able to find any projects that use it on Github

stefcoetzee
1 replies
17h47m

I've only kicked the tires, not used it in anger.

The tutorials are fairly accessible: https://cljdoc.org/d/missionary/missionary/b.33/doc/readme/t...

In a recent London Clojurians talk, the author Leo Noel explained Electric can be thought of as a frontend for Missionary. Rama is by a different team (RPL), but agreed, their philosophies are similar.

contrarian1234
0 replies
16h38m

Oh they updated the docs! Then I'll have to give it another look. I tried to make a simple subscription/memoization setup but I couldn't figure it out. They have a lot of keywords/terms (task/flow/etc.) that a bit hard to understand.. and even when you make sense of them isolation it was still a bit unclear how to compose them properly. The resulting code had to specify a lot of things and was more verbose than I had hoped for

I'll be honest, part of the problem is I don't work in the web space. So I don't care for (or completely understand) the things they're looking to solve (ex: server/client sync issues). I just want to have simpler state-management and have changes propagate across my program in a way that makes my life easier :) (think like a "notebook" where you update values and everything gets reevaluated where-needed)

aphyr
0 replies
1d6h

Nope! For a few reasons Jepsen sticks very closely to Traditional JVM Threads.

Part of that is because Jepsen isn't hyper-concurrent. Most concurrency errors manifest in just a handful of client threads, and the JVM will run ~10K threads without too many issues.

Another part is that I spend a lot of time debugging and profiling. Core.async turns stacktraces into Swiss cheese--it has to, to perform its implicit continuation-passing magic. But that can make it really hard to figure out Who Called What When, and Why Is This Process Slow. Standard blocking IO calls are also something that my profiler (YourKit) can analyze well.

Finally, I invest a lot of time into tuning Jepsen for speed, and manage the handoff of data between threads carefully. In a system where actors were handing off state constantly core.async would be a bigger help, but there's really only a couple shapes for concurrent handoff in Jepsen, and they're addressed by either j.u.c. queues, forkjoin-style structured concurrency, or Weird Transactional Threadpool stuff.

For a little more on this, take a look at https://github.com/aphyr/dom-top or https://github.com/jepsen-io/history/blob/main/src/jepsen/hi...

727564797069706
5 replies
1d13h

Clojure does many things right and I like it for it a lot.

One thing that I see slowing down real teams building real web stuff with Clojure is the near-dogmatic desire to compose (dare I say hack) everything together from tons of libraries that each have their own understanding of how things should be.

I bet Clojure would be much more popular and nicer to work with if the community would _also_ (i.e. in addition to the current approach) have actively-maintained, batteries-included, generally usable web framework(s). (Is Biff it?)

So that there would be alternative path to choose for us who like the language, but are not fans of the build-your-own-monster approach.

Disclaimer: I have been spoiled by Ruby on Rails

Capricorn2481
1 replies
1d7h

Biff is very batteries included, but because it uses a couple defaults that are not particularly mainstream (XTDB instead of SQL, HTMX/Hyperscript) I imagine newcomers will still have a hard time in it. Learning Clojure, AND graph databases, and potentially HTMX is a bit much. Still, a lot of it is just setup for you. Auth is thoughtful and easy and the server config files are well commented. There are deploy scripts to setup a server in linux and SSH into the production REPL. And if you want to use XTDB, the way Biff has setup document validation is really nice.

I also like using it more than an app running on ClojureScript, personally. There are a lot of ClojureScript wrappers around React, and it's almost a meme. As a react developer, I have not found any of them easier than React. I find them kind of convoluted and un-ergonomic. However, HTMX/Hyperscript works really well with Clojure, because their HTML syntax is just arrays and objects that are easy to manipulate. It's the first time that the whole "code is data" proverb repeated by Clojure enthusiasts has rung true for me.

jacobobryant
0 replies
1d4h

First-class SQL support will be really nice when XTDB2 becomes stable--I'm planning to make that the default in Biff for the sake of familiarity, then anyone who wants to can switch over to Datalog easily.

poidos
0 replies
1d10h

I like Biff the most out of the ones I’ve tried.

gleenn
0 replies
1d12h

I commented above but check out Luminus or Kit.

fulafel
0 replies
1d6h

Many of us see Clojure's appeal as connected to avoiding tying things together unnecessarily. Don't complect, compose, etc as in the famous Rich Hickey talk (https://github.com/matthiasn/talk-transcripts/blob/master/Hi...).

metadat
3 replies
1d16h

Jepsen’s (gosh) about a decade old now

I thought Jepsen is more like 12 years old now, going by when I first heard about it circa 2011.

tomnipotent
0 replies
1d16h

The song that inspired the name was released in 2011. Aphyr's "Call Me Maybe" blog series/talk started in 2013, and the commercial audits we've come to love came later.

michaelsbradley
0 replies
1d16h

less than 15, “about” can go +/-

dllthomas
0 replies
1d16h

From a certain perspective, Jepsen is 38 years old.

leoszliard
3 replies
1d16h

Also, Clojure's fun

scary-size
1 replies
1d10h

I think that captures perfectly! I've actually written about that sentiment in the past [1]. The tricky thing with a very "fun" language is that you assume that you can solve everything with it. You end up writing a lot of code, when, in the end, no code would have been an acceptable solution.

[1] https://franz.hamburg/writing/clojure-makes-happy.html

wavemode
0 replies
8h43m

lol sounds like you need to ditch paredit for parinfer

resonious
0 replies
1d15h

This is underrated. I know it sounds unprofessional, but funness is also the reason Basecamp and Ruby on Rails was built in Ruby. Rails dominated web dev in the 2010s and tons of super successful startups were built on it. So while many folks like to scoff at engineers "playing with toys", a lot of good things we have now used to be toys (edit: Linux is another good example of this)

dig1
2 replies
1d12h

Clojure does have significant drawbacks. It has a small engineering community and no (broadly-accepted, successful) static typing system.

I tried to dabble with OCaml multiple times in the past, and what I didn't like was the overly strict type system. I spent more time trying to please the compiler than writing the code. With Clojure (Common Lisp and C), I have more freedom to bend the compiler (type system) to do what I want it to do. Sure, there are pros and cons to that, but I don't want distractions when I write the code. It may be my style, but I meet numerous people who think the same. I believe (static) type systems are a matter of preference, and another camp thinks otherwise. However, it is always lovely to have opposite camps around because we learn a lot from each other.

Working with JVM primitives can be frustrating without dropping to Java; I do this on occasion.

Rich (Clojure author) mentioned multiple times that Clojure is a hosted language, so it is OK to jump into Java when you have to (or javascript). IMHO, this is one of the most powerful features of the Clojure ecosystem because it is way more pragmatic. I've seen numerous attempts to re-implement the same thing repeatedly in your favorite language just because you don't want to touch "a library written in That Other Filthy language because it is slow/insecure/not-flexible/you-name-it". This was/is very frequent in Common Lisp but also in the Rust communities.

tmtvl
0 replies
1d11h

Having type checking is indeed a matter of preference. I personally quite like having the language protect me from making silly mistakes, which is why I use type declarations liberally in my Common Lisp code. With SBCL doing its best to ensure they hold it has probably saved me minutes of headaches each project.

Also CL is very pragmatic and nobody bats an eye at, for example, woo being based on a C library.

Karrot_Kream
0 replies
1d2h

I believe (static) type systems are a matter of preference, and another camp thinks otherwise.

It's funny, when I led high reliability teams at a Big Tech, I wanted static strict typing everywhere done yesterday. Every production problem of ours was related to small, subtle issues. Upgrading the runtime leading to JSON key reordering breaking some ancient client that was hand-unrolling JSON. Some library deep within our stack started returning HTTP codes as ints and not strings which broke some error parsing of ours. And every outage was a big deal as the scale we operated at meant that if even a small percentage of customers were affected, we'd have hundreds of problem reports.

Now that I'm working at a tiny startup things couldn't be more different. We chose to go with a static language because that's where the mindshare seems to be, but I am missing working in a dynamic language. Because of the speed with which we're shipping our code quality is really bad. In the interests of time, instead of dealing with the type system and refactoring things cleanly, we'll just make multiple copies of the same object structure with small differences and use them all over the place. We copy/paste the same logic in 10 different places instead of isolating the functionality and referring to it in different places, because the shapes of types make things a bit tricky to make generic.

I find that different problem spaces lend themselves to different solutions. Are you getting paged in the middle of the night for subtle bugs? Then strict static typing is probably for you. Are you iterating quickly and can problems be solved by someone sending you an email or Slack? Dynamic typing is probably better. The trick is to not accumulate too much debt in the dynamic paradigm in case you see growth to port things over to a static paradigm.

I've seen numerous attempts to re-implement the same thing repeatedly in your favorite language just because you don't want to touch "a library written in That Other Filthy language because it is slow/insecure/not-flexible/you-name-it". This was/is very frequent in Common Lisp but also in the Rust communities.

I think languages with lots of enthusiasts just end up with these attitudes unfortunately.

avbanks
2 replies
1d7h

"Almost every database has a JVM client, typically written in Java, and Clojure has decent Java interop."

Curious to know in what ways could Java interop be better in Clojure?

aphyr
0 replies
1d7h

Clojure's Java interop story is generally really good. Method calls are predictable and uniform. The collection types generally work both ways, rather than being a completely independent type hierarchy (as a former Scala programmer, what a breath of fresh air). Functions work as Runnable, Callable, Comparator, etc. Reify, deftype, and proxy are sufficient for maybe 98% of APIs I've worked with.

There are, however, those rare exceptions! For instance, some Java APIs literally won't work without annotations. There's just no way (last I checked) to express those in Clojure.

I don't remember exactly the nature of this problem, but I've had occasional issues with deeply overloaded argument dispatch before. Maybe it was something like "The Java API expects an object x which responds to x.foo(long l) and also x.foo(int i) differently", and there was no way to convince deftype to emit the right kind of class.

AOT is... it's weird. You want ahead-of-time compilation when, say, you want to write a Java class that uses datatypes defined in Clojure. But AOT, at least in Leiningen, tends to break stuff in truly inscrutable ways. My professional opinion is that it's haunted.

Capricorn2481
0 replies
1d7h

I'm not the most knowledgeable on this, but in general, Clojure has smart ways to convert data types from Java types to Clojure ones. Just like Clojure lets you use functions on a lot of different data types (mapping over vectors, objects, etc), it lets you do the same with Java collections.

Instantiating objects and using their methods is pretty terse and easy when using macros like ->

(def method-result (-> (SomeClass.) (.someMethod "arg1")))

silcoon
1 replies
1d15h

For all of you new to Clojure, check out this interactive tutorial: https://tryclojure.org

chii
0 replies
1d14h

https://4clojure.oxal.org/

that is an even better interactive set of lessons starting from easy to intermediate to hard.

masijo
1 replies
1d6h

Clojure is my favorite language by far, it feels natural to me.

That said, the two things which I truly miss from other languages are: understandable error messages (as mentioned by other people) and typing hints.

I've had to refactor large code at work and it's been a pain. It's hard to know when a function will throw an exception or just return nil, it's hard to know if a core function will return a vector, a sequence, or another type of collection, and so on and on. There are some projects that try to address this (spec, malli, typed clojure) but I haven't found them to be a good replacement to something among the lines of python/typescript typing.

Spivak
0 replies
1d6h

Maybe I'm just spoiled by modern languages that have this and the ones that don't are bolting it on (Python/TS) but how do you put up with it? Any language that can't tell me the what goes in / comes out of a function feels borderline worthless for a codebase more than a few files.

Trying to infer types by looking at what the current code appears to be able to do the objects is just pain for its own sake.

jrvarela56
1 replies
1d6h

I have been acquainted with Clojure for a few years now. Haven't been able to 'break in' despite several attempts.

My main conclusion is this: the language/workflow make developers way more productive compared to other modern stacks (I do Python/Ruby/TS on my day to day). The biggest, unsolved problem is http://www.winestockwebdesign.com/Essays/Lisp_Curse.html

tl;dr It is so easy to implement things in Lisp so you end up with a myriad of half-baked solutions and no long-term support of dependencies.

iLemming
0 replies
1h14m

I'm not sure if the Lisp Curse can in fact be easily applied to Clojure. Clojure projects, for some reason (in my opinion), don't suffer as much as projects in other Lisps - such as Common Lisp and Emacs Lisp. Maybe it's because of the hosted nature of the language, there's usually a good set of libraries to choose from for Java and Javascript, and my guess is that this would hold true for ClojureDart and ClojureCLR as well.

You can typically find some Clojure/Clojurescript wrappers if you want to interact with a JVM or Javascript library. But if a wrapper doesn't do what you need, you can always use interop directly.

Also, Clojurists tend to adhere to some good stylistic defaults. They try not to overcomplicate things without good reasons. Furthermore, the nature of the language forces you to keep things small and simple.

ReleaseCandidat
1 replies
1d13h
roughly
0 replies
1d2h

I really appreciate Aphyr for being one of the only people I've seen to fully integrate their entire selves into their public persona and accomplish success. The Jepsen reports on any given system are effectively canonical documentation for a product, and Kyle's work and writing on concurrency overall is fantastic. At the same time, there's no "suit and tie" Aphyr out there - you read the blog, you get the weird; you follow them on social media, well, you get the Full Aphyr. I respect that they've been willing to put their whole person out there and have been so goddamn good at what they do that they've been successful at it - it's genuinely inspiring.

yayitswei
0 replies
1d9h

Love that Rich jumped in in the comments!

swiftlyTyped
0 replies
1d9h

I love clojure, but over time I've learned the painful lesson that at the end of the day, for complex modern applications, the ecosystem is all.

That said Clojure ecosystem isn't too bad

kaliszad
0 replies
1d12h

Can fully agree that Clojure(Script) really is a blessing and a great choice for many projects smallish or big. Frankly, Clojure is the first language I did professional programming in, with a little bit of experience in C, Python, PowerShell, Java, Bash, SQL before to solve university assignments or to help with systems administration/ some analytics of traffic dumps etc. We program OrgPad in Clojure+ClojureScript and it was a good decision, even as we have more than 100kloc of code. It is approachable and if I write a bit of code, I am fairly certain that it will do what I imagine. I didn't have that to that degree not even in Python, PowerShell or Bash, it is a completely different league, quite possibly game.

The REPL workflow is really nice, my colleague did a video about it: https://www.youtube.com/watch?v=4igO7Qbyj9o

Nowadays, even some functions/ short tasks or small projects can be served by Babashka/ nbb in places where you would often consider switching from Bash to Python.

jgrodziski
0 replies
1d11h

We also write about our choice of Clojure for application development here: https://defsquare.io/blog/why-we-bet-on-clojure

fuzztester
0 replies
1d

sorry guys, i don't know clojure, so i'll write this script in my favorite programming language:

#/usr/bin/english

if clojure is mentioned, can rich hickey be far behind?

and if rich is mentioned, can this video of his be far behind?

methinks not.

lo, and behold, said video:

https://youtu.be/f84n5oFoZBc?si=Fy-1QcLkIiXVnqPo

dsign
0 replies
1d13h

This is what happens:

- Dev Foor Barry needs to solve problem X. Mr. Barry thinks about the problem for a long time, picks a technology stack YZ that is suitable to the solution and to his experience, and goes to work.

- Some time later, Mr. Barry meets dev Alice Mallory. She didn't know X was a problem, and she didn't know Mr. Barry either. But less than a minute into their conversation and being a mutual acquaintance, Ms. Mallory is convinced that technology stack YZ is the WRONG tool for the job. She can't stop herself from asking "Foor, why YZ? Wouldn't it have been better to use KY?"

I've been involved in this ploy (sometimes as Mr. Barry, and sometimes as Ms. Mallory) any number of times, for any number of technologies, from mainstream ones to more obscure ones. For the last few years, I've been trying to act more as Steve: "Use whatever you want." I have to admit though, that kind of acceptance is a difficult, uphill spiritual path. And I have met plenty of architects in their sixties and seventies who saw the hill and took a wide berth, and keep acting like Mallory.

barrenko
0 replies
1d9h

There is no Rails for Clojure because Clojure is Rails for the right kind of software architect. Clojure is a language that beat the crap out of me but in a positive way. I don't use it, but learned a lot about what is possible.