Clojure as a dependency
blog.danieljanus.pl
blog.danieljanus.pl
Which is about 8 years old.
Clojure hold a soft spot in my heart for emotional reason, but the engineering behind it is top notch.
If any of you need motivation to go and learn it! Please do it! You will never regret invest time in clojure.
I've written about this (free online) book here on HN before but it really gave me the kick I needed (and lightweight humor) to actually start using Clojure(Script) professionally, so if you're on the edge, give "Clojure for the Brave and True" a try!
https://www.braveclojure.com/clojure-for-the-brave-and-true/
> The lessons are carefully thought out and unique. From learning asynchronicity to the lyrics of Lady Gaga, to learning about thread lock in the context of drunken dwarves, every chapter is fresh and (in all honesty) beautiful. - Artur Malabarba, CIDER maintainer
Agree that Clojure is a good language to learn. If you plan to use it beyond educational purposes though, expect to hit many walls when/if you plan to write real world apps with it. The foundations are there but the ecosystem isn't. Someone will mention the Java libraries, but at that point you are not really writing Clojure. Also, be prepared to deal with the cultish obsession of the community. They have very strong opinions but will never elaborate.
Uh, Clojure was literally written from day one on the founding principle of Java interop? Using Java libraries from a functional lisp is literally the reason Clojure exists.
I'm curious to know what "cultish obsession" you are referring to. Also, what strong opinions have you been unable to get elaboration on?
There's a little wiggle room; I think it's largely dependent on what you define each by, but basically anything that forces me to write my app in a certain way, and limits how easily I can slip in things that are clearly libraries, become liabilities, and I would prefer to avoid.
Obviously that's hand-wavey; there are plenty of things that people will call frameworks that nevertheless dictate very little, if anything, about the structure of your code, and which are easy to get to play nicely with other libraries. But there are also plenty that...don't. But to play it safe, I look at things that described to me as frameworks with far more suspicion than I do things described as libraries.
A previous employer of mine was a Clojure-heavy shop. There were multiple teams. A Clojure dev going from one team to another could expect to find themselves in a very different stack with very different conventions to do very similar-looking web service tasks. In essence, each team had composed their own under-documented framework.
Note that it doesn't mean you can't use a framework...it just means prioritizing ones that feel more like libraries, and minimize their surface area.
I've worked in a shop that had 8 dev teams, all working on different systems, all in Node, using Express. We favored standardizing, but standardizing on something easily extensible. The middleware approach made including non-standard things really easy; if I needed to include some custom authentication library that wasn't written for Express, it was trivial to do it; a couple lines of glue was all it would take, even if it included some initialization.
Compare that to a Spring, say. I have to know about the framework's authentication interfaces, implement a comparatively large amount of glue in terms of those interfaces and configuration, and even more hoops to jump through if there's state I need to initialize first for the underlying library.
That's what I mean. I'm not arguing for or against standardization, but extensibility and minimal cognitive overhead in extending it.
What I found through my experience is that offering many hooks for extensibility sometimes increased cognitive overhead for the team. With so many ways to accomplish a given task, no agreement on how to do it, and no guiding conventions, moving from one part of a repo to another can entail nontrivial cognitive overhead. Moving to a different project with a different collection of extensibility approaches employed for the minimal cognitive overhead of those authors sometimes compounds the problems. I've seen developers do things that were easy, obvious, and extensible to them but utterly incomprehensible for everyone else. Having the freedom to compose libraries as they saw fit may have made it more likely.
Compare to Rails. For all its many, many warts and often exceptionally opinionated quirks, it's often relatively easy to move a junior or mid-level dev from one part of the codebase to another. Or one team to another. Or even one company to another. Many of the conventions are the same and the standards largely match, making onboarding easier. This can also make it easier for a random dev to predict how the system might be extended for a given task, easing maintenance over time.
I think the core tradeoff here is between what minimizes overhead and cognitive costs for an individual and what is optimal for an organization. Also, I ran into issues with Clojure teams composing their own stacks and forgetting things like basic security measures of the sort that mature, pre-fab frameworks like Rails build in by default. They minimized the surface area so hard that they maximized the ignorance they exposed, producing services that couldn't be bothered use CSRF tokens because the developers had never heard of them.
Again, I think you're right. It's about extensibility and cognitive overhead. I've just found there to be some nuance to what approach provides the best outcomes. I don't particularly enjoy being bossed around by a bucket of bits that tells me how I'm allowed to express my wishes, but I've found that my emotional experience of it isn't the important thing.
What you might be encountering is after a few years of Clojure becomes such that you'd rather not be writing anything else and that is semi-practical as opposed to many other niche specialised languages where it's not so realistic
Libraries, not frameworks is a point of contention even within the Clojure community, libraries tend to compose and upgrade better over time but there are also frameworks in Clojure
Luckily we don't have one that dominates the language so totally that it cannot be ignored despite what inevitable deficiencies it has
Keep in mind most Clojure developers came from other languages and have Vietnam flash-backs to many frameworks
I also found the points about interop to be nearly academic. The teams I worked with went to significant amounts of trouble to avoid using it whenever they could.
That's hardly true, not even clojure.org claims that. Interop is far from seamless.
>Clojure is designed to be a hosted language, sharing the JVM type system, GC, threads etc. All functions are compiled to JVM bytecode. Clojure is a great Java library consumer, offering the dot-target-member notation for calls to Java. Clojure supports the dynamic implementation of Java interfaces and classes.
What do you mean by "far from seamless"?
I think this originated in a conj talk several years ago. However, the speaker mentioned that it was simply too early to have Clojure frameworks, but that in a few years they will be helpful. Can't find the talk at the moment unfortunately, may have been by Alex Miller maybe in 2011 - 2013.
I mean, that's more widespread than just Clojure; it's a generally-held belief in most of the functional programming world.
A library is just a bag of functions you can reuse at your leisure, hooking them up as you wish, calling the ones you like, passing references to the ones you like, and replacing or dropping the rest. It's a box of LEGOs. You can mix it into a big box with your own bricks and other libraries' bricks, passing the closure from X to the HOF from Y, and it all just works together, because nothing's expecting anything more specific than "data of the right type" or "a function of the right type", where the types are all basic core-language data types.
A framework already plugs some parts together for you, in a way that means you can't just replace some of those components with something better or more suited to your use-case. It's more like a molded plastic figurine that happens to have some explicit LEGO attachment points on it.
I personally know which one I'd prefer to find, upon opening up the docs for a new library. Every framework I've ever used, I've eventually had to fork in order to have an app inject some bit of customization into its behavior in a place it wasn't expecting such customization would be needed. Libraries don't have this problem.
Libraries-over-frameworks is also a bit of old-school Lisp-ism poking through. Regular programming languages tend to do things like web apps or games in terms of an opaque framework driver piece, that gets configuration handed to it in some descriptive format. To someone coming from a Lisp, that's silly: what better configuration format is there, than a programming language? Especially one tuned for expressing the app's domain? What a Lisper would expect a Lisp web-app library or game engine to do, is to build up and up from functional primitives into a high-level DSL, and then have the user write the final driver code in terms of the library's DSL. Then, the "configuration" is just the logic of that driver program—which can also drop out and replace any part of the library it wants, just by calling/passing a different function than the one you'd expect.
You can see this contrast in Emacs vs. vi:
• vi is a native program, that upon startup, parses a bunch of files in a custom configuration language, where each directive triggers its own special change to vi's internal state.
• Emacs is a Lisp runtime, where all the stuff that makes Emacs Emacs, is implemented as a set of ELisp libraries which call into said runtime. Each Emacs "major mode" is just an ELisp program that calls into those libraries. And your toplevel global configuration is just an ELisp program that defines the functions that are hooked to call into the major modes.
Laughed at the "How to refactor? Just don't write code that needs refactoring, simple made easy!"
So with this, are there other languages where they are explicitly designed to be "parasites" like Clojure?
One could argue the many "compiled to JS" language are such, but they are always very hardcoded to JS, so not really parasitic like Clojure.
Clojure was definitely designed to be both embedded and hosted, but those are distinct axes.
There are lots of languages designed to be embedded as scripting or automation runtimes in other systems. Tcl, Lua, Python, Guile (a lisp)...
Why embedded- pre-web, there used to be a practice of writing your infrastructure in systems language (C/C++) then writing your application tier in a scripting language. Countless games and GUI line of business tools on Windows were written this way.
Java in this case was considered a "systems" language, and there is a long history of embedded languages for Java. My favorite JVM-embedded language was called Beanshell.
This embedding was/is a use case for Clojure, important for "sneaking" Clojure in to the enterprise.
The "designed" part implies that the scripting runtime plays nicely with process-owned resources like threads and memory, and has a well defined interface for exposing your infrastructure objects to the scripting layer.
Post-web, if you squint, you can recognize this architecture in the DOM the browser (written in a systems language like C++ or Rust) exposes to its scripting tier (Javascript).
This is typographic patriotism on my part (I'm Polish). This typeface [1] is a design of the late Andrzej Heidrich, who also designed the banknotes of the Polish zloty.
One of my two favourite Polish typefaces. The other is Antykwa Toruńska (Torunian Antiqua) [2], a 1960 design by Zygfryd Gardzielewski.
https://nelsonmorris.net/2012/07/31/do-not-use-version-range...
https://hypirion.com/musings/lockfiles-are-not-the-only-opti... https://stackoverflow.com/questions/44521542/why-doesnt-grad...
I'm all up for saying "thou shalt specify precise versions of library dependencies", but the specific case of `org.clojure/clojure` seems qualitatively different in that a precise version influences your consumers, and leaving it unspecified means your library might not work at all.
I'm not sure I understand what issue you would solve with using "[org.clojure/clojure ">1.2.0"]" compared to what we're doing today, which is specifying the exact version we're using. What is the issue you're addressing here exactly?
In general, the Clojure community favors stability (more than others like npm) when it comes to dependencies, so you're almost always defining the exact version of the dependency you want.
I'm not sure I understand how `:scope "provided"` plays out if two dependencies say they want different versions of clojure, but that's also relevant.
That sounds like a solution to some problem, but I'm unsure of what that problem is. Not sure it makes sense to depend on more than one version at a time, because you're usually just testing with one version and can ensure compatibility with just one version.
Semver somehow made people believe that we can have versions numbers created with human-intuition but used as they are 100% true and generated by robots.
Look at it this way: say library A expresses a dependency on clojure 1.8, and library B on clojure 1.9. As the author of application C wanting to depend on both, what do I do when I want to use 1.10?