Reading the final chapters of Brave Clojure now, and I am being introduced to protocols and records. I am very suspicious of this, especially records. It sounds just like place-oriented programming, which is what I came to Clojure to avoid. What does the "Clojure elite" think of this? Am I missing something here? The author claims—without further explanation—that "they will help you write cleaner code". Sounds exactly like something my OOP coworkers would say. To be fair, the author did mention that there are other concepts (deftype, reify, proxy) that he won't cover. Perhaps those are the bits I need to see past this?
@soltanzadehramin also deftype, defrecord and maps have their trade offs between memory footprint and ergonomics.
(deftype T [^long a ^long b])
(T. 1 2) ;; 32 B
(defrecord R [^long a ^long b])
(R. 1 2) ;; 48 B
{:a 1 :b 2} ;; 472 B
When memory footprint is important (let's say you are going to have millions of them) deftype gives you the bare minimum but it is annoying to work with since all fields are private, so you need to make it implement a protocol. Next you have defrecord, which is nicer because it is an enhanced deftype which already implements a bunch of Clojure interfaces, and can have meta, but it is a little bit heavier on memory because it will add extra fields for meta and the extra keys it support. Finally maps are the most versatile, but as you see there they take more memory which can or can not be an issue in your situation.relevant from another author of a major Clojure book https://clojurians.slack.com/archives/C03S1KBA2/p1724706831558599?thread_ts=1724706593.717949&cid=C03S1KBA2
I wouldn't say the problem with protocols and records is that they're place oriented. I think maps might be preferred over records/protocols mainly because records/protocols aren't necessary in many cases.
That's good to see. Is it not normal to try to avoid rigid domain models in Clojure, per the philosophy of Datomic (or at least my interpretation of it)? Part of the appeal of Datomic in my mind (keep in mind I've never used it, so I may be completely wrong here) is that you avoid rigid structures that in practice end up having nullable fields (and thus only giving the illusion of rigidity). Record types strike me as being in conflict with the philosophy of avoiding the billion dollar mistake. Getting back fully on topic, I realize I might have been mistaken regarding PLOP. If records are immutable then I understand it a bit more. For some reason I got the impression that they weren't.
Protocols are the natural outcome of observing that sometimes single dispatch over type really is the right abstraction and that every language under the sun supports it efficiently, with some runtime cost to ensure that they are extensible unlike interfaces. Clojure itself clearly doesn't eschew polymorphism, given the existance of the defmulti suite.
(though multimethods now fill that niche quite well)
Regarding place orientation in records. You don't need to require any keys:
(defrecord AFoo [])
(-> (->AFoo)
(assoc :bar 42
:baz 96))
You can always assoc more keys if you want.The reason for having a record type without any required keys is that you can still get the performance benefits of protocols and interfaces.
I don't use record that often. When I do, I often don't require any keys. I'm not sure if that's weird or not, but it seems to work well.
I see. It was probably partly a knee-jerk reaction from me to assume this was wrapped OOP. Had I read the docs first I probably would have understood a bit more before asking. I do see that the docs do mention using records for domain models. Even if you can assoc more keys, you can't dissoc the defined ones. I suppose that is fine if you are disciplined enough to avoid nillable keys. Feels like playing with fire, though.
My point was that you can use defrecord just fine without any defined keys.
Yes, that I have no problems with. I suppose my suspicion can be boiled down to using records with defined keys, which seems to be condoned (by authors and the docs). I suppose there's no stopping you from ever removing a key from a definition, though, right? All you do is lower the requirements, which should be completely backwards-compatible. I suppose that makes sense, yeah.
If you know that key is required and will be present, then you can include it in the defrecord definition as an optimization.
> I suppose there's no stopping you from ever removing a key from a definition, though, right? All you do is lower the requirements, which should be completely backwards-compatible.
Usually. There is a different between {:foo nil} and {}, which may or may not be important depending on the use case. There may be some cases where the key's presence or absence is important and nil is an allowable value.
IMO that part of records is just wonky design or at most something that is lost to modern Clojure. There clearly is an effort to make them act like maps (they implement all the map API bar transients with no overrides possible and even dissoc to a plain map if they were to lose part of their basis), but then you have things like unprovided args in the map ctor resulting in nil (which is inconsistent with the above) and = taking type into account as well as structure, which means they don't compare = to a plain map with the same entries.
If I were to design them today, I'd be tempted to split them into two concepts: a map implementation that really acts like a normal map in all aspects but is highly optimized towards the type being known and only their basis keys being manipulated, and something very close to deftype but with obligate immutability, the current defrecord behavior for =, and only the ILookup and IObj interfaces to act like custom primitive values with a thin layer of Clojure amenity (which would more or less put them in the same place as always-immutable Common Lisp structs). Then I'd probably chuck the first "part" away because it's quite unlike Clojure to introduce "X but it is marginally faster if Y holds".
Hash-map with a specification library has been the approached used in all the commercial projects I've worked on sinc 2018. There was one project before that which dispatched on type of a value unwrapped from protobuf messages. Protobuf messages were generated by dozens of different applications written in many different languages.
What are people's preferred ways to start a REPL with deps.edn? I'm not sure if I'm really doing it right. The closest I have found is to make an exec-fn from deps.edn in every project, that runs this
(defn start-repl-server
[& args]
(let [args ["--port" "7888" "-b" "0.0.0.0" "--middleware" "[cider.nrepl/cider-middleware]"]
server (apply nrepl-cmd/-main
args)]
(println "nrepl started"
(pr-str args))))
Then I can connect to the repl with Calva easily@crimsonhawk47 I'd caution against binding to 0.0.0.0 for something as powerful as a Clojure REPL unless you absolutely need to. The security implications can be troubling, depending on the rest of your setup.
That looks right to me. Though I most often let Calva start it for me instead. Calva: Start a Project REPL and Connect -> deps.edn.
That works most of the time. I was running into an issue when restarting that REPL (like if I update deps.edn). I close the old one and run that command. Sometimes when it restarts, a new terminal window is not made, so I can't close it again without closing all of VSCode
I’m not quite following with “a new terminal window is not made”. And what does “I close the old one” imply? You don’t need to close the repl to restart it, though. If you run Calva: Start a Project REPL and Connect, the current repl will be killed and a new one started.
A terminal was being created in vs code when running that command, and I was closing that to stop the REPL
I've seen this behavior also lately. When jacking in after having killed the old jack in, the Calva Jack-In entry in the terminal panel doesn't show up.
So only the bash entry is shown despite the jack in
i see you are using port 7888 probably to let clojure-mcp talk to your repl as well. An alternative is listed here https://clojurians.slack.com/archives/C068E9L5M2Q/p1749561380910559 Here you just let calva start your repl as normal and then expose the port 7888 manually
I generally don't have anything per-project. I have my user deps.edn file here https://github.com/seancorfield/dot-clojure and generally start a REPL from the command-line via the :dev/repl alias in that file (lazily, I use the bin/repl script there).
Starting the REPL manually in a terminal means that it survives a reload of VS Code (to pick up extension updates). I don't restart my REPLs very often (and I don't restart VS Code very code) -- I just leave them running for days (weeks sometimes).
big fan of repls started outside of the editor. removes one level of obscurity. but didn’t know if that was more to chew than desired here
> A terminal was being created in vs code when running that command, and I was closing that to stop the REPL I don’t think I have even considered that someone may try to kill the repl this way. But when you tell me, it is very obvious that people would try that. That’s how a regular terminal behaves. Calva should recreate that terminal if it has been closed. You are welcome to file an issue about it. Still. No need to close the old repl when you restart. Just start the repl and that will be a restart.
Also. With Clojure 1.12 you don’t need to restart the repl to pick up changes in deps.edn. There’s a sync-deps function that will dynamically do it in the running repl. If you install the Calva Power Tools extension you will get a command for doing this.
I have the following, adapted from practicalli—criticism welcome:
:repl/inspect
{:extra-deps
{nrepl/nrepl {:mvn/version "1.3.1"}
cider/cider-nrepl {:mvn/version "0.56.0"}
fipp/fipp {:mvn/version "0.6.27"} ; cider wants this
djblue/portal {:mvn/version "0.59.0"}
refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"}
org.rksm/suitable {:mvn/version "0.6.2"}
org.clojars.abhinav/snitch {:mvn/version "0.1.16"} }
:main-opts ["--eval" "(require '[portal.api :as p]) (add-tap #'p/submit) (require 'clojure.repl.deps) (require '[clojure.reflect :as reflect]) (require '[snitch.core :refer [defn* defmethod* *fn *let] :as snitch])"
"--eval" "(apply require clojure.main/repl-requires)"
"--main" "nrepl.cmdline"
"--middleware"
"[cider.nrepl/cider-middleware,suitable.middleware/wrap-complete,portal.nrepl/wrap-portal]"]}@james.amberger Nice!
Hi! I'm new to Clojure and I'm having trouble to identify if a library is stable, active or legacy. How does y'all identify it and choose a library to use? I'm coming from JS/TS, Go and Elixir where the last commits says +/- the maintenance state of a lib, but it seems to don't be feasible or reliable in Clojure.
I would say looking at the author, and if they are still active on GitHub, even if not on that specific library, but at least you'd know if say you encountered an issue and created one on the repo that they would look at it because they're still active as a contributor. Otherwise there are some libs you just hear a lot about, so know they are popular. And there are some groups like clj-commons that maintain popular but no longer maintained libs, so anything from clj-commons is generally active.
Indeed. Libraries in Clojure often have a very narrow focus so they can sometimes be "done" and not need any maintenance. In addition, Clojurians are generally very cautious about breaking backward compatibility so changes tend to be much more measured (and therefore safe). Several of the libraries we use in production at work haven't been updated for years -- and that's just fine: they work great (across multiple Clojure versions) and they don't need any features added.
additionally, the surfaces where you would see likely security concerns (eg. media format parsing, web servers etc.) are typically java libs used via interop, and they have clear standards for security updates and stability
also, these things have a much better track record in the jvm than the C based libs js would be using
I asked the exactly same question to a Clojure developer in Twitter DM and he answered to me the same thing seancorfield said, Clojure is a language that is more stable than other languages like Scala/JavaScript/etc, so it is very common to find libraries in ALPHA but used in production by companies. https://dl.acm.org/doi/pdf/10.1145/3386321 (Sorry admins if I couldn't send links here) This is a document created by Clojure's owner/developer/founder, he says that Clojure is projected to be a stable language, ready-for-production, it is also possible to see in the document an example, a graph comparing Scala breaking changes and Clojure breaking changes (page 26-27)
Regarding stability: at work, we've used Clojure since early 2011. We initially went to production on a prerelease of Clojure 1.3, and we've run alpha/beta versions of every release of Clojure in production ever since. Code that we wrote back in 2011 for Clojure 1.3 mostly runs unchanged on Clojure 1.12!
Regarding specific libraries that you're curious about: ask! Folks will be happy to relay their experiences with libraries here.
I agree it and really think that is a awesome thing - is a piece of confidence in a sea of ever changing ecossystems - but sometimes I feel I've missed my reference point to identify a well-maintened library. But your responses tranquilized me, thanks!
Another way to go about it is to look for conversation (or start a conversation) about a tool you are considering, and share the purpose you have in mind for it..
Stability means we seldom have to rewrite to work around incompatible changes that were thrust upon us... but stability does not imply you won't find lots of strong opinions!