Why is Jepsen written in Clojure?
aphyr.com
aphyr.com
* 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.
Developer experience is fantastic too with HMR, REPL and cljc files. It's fun to write Clojure code :)
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?
> immutability
Make a case for immutability being so critical to HMR. How does an immutable data allow for swapping of code.
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.
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.
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
I don't remember what your describing being a feature of SLIME, but it's been 10+ years so I may just not remember.
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.
Calva is a solid choice if you don't mind switching editors.
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.
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.
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.
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
- 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
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.
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?
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
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.
And that's why I use good frameworks.
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.
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.
Thank you for sharing it!
This year's funding by Clojurists Together allowed me to spend a lot of time completely rewriting large sections of the site to bring it up-to-date with modern Clojure tooling and libraries -- but I know there's still room for improvement.
I love the idea of a community investing in this type of outreach (beginner oriented) and I'll be happy to contribute when I can.
[: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 ?
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.
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.
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.
> 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?
If it is your code, or your company's code, why not? But think about dependencies!
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.
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:
Three!? Ridiculous! We need to develop one universal Clojure environment that covers everyone's use cases.
Situation: There are fourteen competing Clojure development environments.
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!
They've been exploring going on 16 years. It's time to stop exploring and start exploiting.
> 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.
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?
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.
Given this end the fact that many clojure people come from Java, it does not surprise me at all to see this attitude.
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).
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.
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.
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.
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.
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.
(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.)
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.
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.
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
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).
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.
"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."
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.
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...
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.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!
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!
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.
Killing me! My brain says this should be (+ clojure clojurescript)
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
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.
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.
Also CL is very pragmatic and nobody bats an eye at, for example, woo being based on a C library.
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.
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.
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.
that is an even better interactive set of lessons starting from easy to intermediate to hard.
That said Clojure ecosystem isn't too bad
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.
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.
user=> (defn foo [[a b] (+ a b))
Syntax error reading source at (REPL:14:26).
Unmatched delimiter: )
user=> (defn foo [a b] a/b)
Syntax error compiling at (REPL:1:1).
No such namespace: a
user=>
It will give you a stack trace of the compiler when asked following a syntax error though, with a disclaimer not to confuse it with your own code: user=> (pst)
Note: The following stack trace applies to the reader or compiler, your code was not executed.
CompilerException Syntax error compiling at (1:1). #:clojure.error{:phase :compile-syntax-check, :line 1, :column 1, :source "NO_SOURCE_PATH"}
clojure.lang.Compiler.analyze (Compiler.java:6808)
clojure.lang.Compiler.analyze (Compiler.java:6745)
[...]
Possibly you were using tooling that always tried to present the stack trace without discerning between kinds of evaluation errors?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.
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.
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".
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. :-)
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?
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...
``` (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?
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 ```
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
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.
- 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.
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. ;-)
This last bit answers that though I think. Thanks.
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!
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.
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.
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...
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) (I) see no advantages for using Clojure instead of Java on the JVM
To be blunt, I think that says more about you than it says about Clojure. A good engineer should always see the tradeoffs.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.
He's basically saying, in Clojure, don't use time-honored, clear idioms that are perfectly appropriate in some other Lisps.
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.
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.
Maybe unrelated (but seemed like a symptom): In my case putting together a web framework was not trivial, as there was no equivalent to Django or Rails. Instead it seemed I was going to have to put together every piece of the stack - I loved it but it was not productive. Deciding between integrant/component, ring/compojure/reitit, etc was easy once you took a bit of time to read code and try it out but the furthest from 'just run it and keep going by tweaking'.
I haven't given up yet, will give it another try this xmas break. I do believe there's huge productivity gains in a repl-driven clojure workflow.
Curious to know in what ways could Java interop be better in Clojure?
Instantiating objects and using their methods is pretty terse and easy when using macros like ->
(def method-result (-> (SomeClass.) (.someMethod "arg1")))
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.
#/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:
(As an aside: Missionary is a higher-level, more recent option in this area.)
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
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.
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)
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...
- 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.
https://aphyr.com/posts/340-reversing-the-technical-intervie...
https://aphyr.com/posts/341-hexing-the-technical-interview
https://aphyr.com/posts/342-typing-the-technical-interview
https://aphyr.com/posts/353-rewriting-the-technical-intervie...
https://aphyr.com/posts/354-unifying-the-technical-interview
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.
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.
Shun the nonbeliever!
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.
"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.")You might enjoy this classic comedy routine: https://www.destroyallsoftware.com/talks/wat
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.
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.
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.
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.
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.
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.
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.
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.
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.
aphyr should've written it in ada, though.
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.
You wouldn't be able to translate Clojure to another language line by line unless you brought in some framework or library
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.
I thought Jepsen is more like 12 years old now, going by when I first heard about it circa 2011.