I like to ideate by just arguing with ChatGPT sometimes, it got me here today: What if there was a clojure-like with a type system that treated data constructed by the program itself (eg a map literal, or an output from a core function given only closed data) separately from dynamic runtime data sourced from the edges and actually being conveyed around the system?
;; REPL: create-user
(defn create-user [{:keys [id name email]}]
(store-user {:id id :name name :email email}))
;; inference:
create-user :: Closed{ id:Int, name:String, email:String } -> Result
store-user param :: Closed{ id:Int, name:String, email:String } -> Void
;; safe assoc (replace email):
(assoc user :email (sanitize-email s)) ;; returns Closed{...}
;; adding an extra field:
(assoc user :last-seen (now)) ;; returns Open (must widen or be explicit)
;; => REPL/Compiler warns: assoc introduced new key :last-seen; consider widen! or annotating
(widen! user) ;; now further assocs preserve Closed with row-var
;; ingest external JSON:
(defn handle-webhook [event] ;; event : Open
(let [user (coerce<User> (:payload event))] ;; runtime-validated
(create-user user))) ;; user is now Closed<User>
Why this design is attractive
• Conservative: closed-by-default preserves invariants where you actually need them (APIs, internal args).
• Practical: you can keep untyped openness for true foreign data.
• Explicit: opening is an explicit act (`open!`), so accidental contamination is unlikely.
• Ergonomic: destructuring + inference gets you low ceremony for the common case (named args → structs).
• Composable: row polymorphism + widen! gives controlled extension when needed.
All maps start out closed.
They only become open when you explicitly open them.
Core functions respect that distinction automatically.
You still get Clojure’s data-centric style — just with compile-time awareness of when your data is yours vs someone else’s.What if "someone else" is also me, but assoc-ing in to one of these maps from a different namespace or when I'm calling this as library code? Now I have to remember namespace or library specific rules for what I can and can't add to a map, or take the extra step of calling open! each time. What do I get in return?
I haven't thought this through at all, but it feels interesting. If something coming out of a library function is open, or not statically determined to be closed, same thing, it stays open unless you do some runtime parsing eg with spec or malli, or build a map from static parts (literal with keywords) vs just transforming the open data.
I think the closed maps and the open data being passed around would stay separate generally. Maybe it's useful to have to call open before you rarely combine them
I think associng closed to closed should stay closed, so that hallucinated example of associng a new field with (now) value should keep it closed. If the value is more complicated than a timestamp and derived from open data, then you have to open it
You've explained (some of) the potential mechanics but not the value proposition. I don't understand the benefits here - at most I am automatically protected from unsafe serialization of data on the wire, but that's far from an everyday problem that needs a solution at the language level. I think you should try to understand why Clojure doesn't do it this way (explained a bit https://dl.acm.org/doi/pdf/10.1145/3386321) before having ChatGPT argue that this is an improvement.
Not sure I've read that yet, but not trying to rehash types vs dynamic languages. I was wondering if there's a lightweight middle ground, specifically around most of the program data being small and statically verifiable and separate from the data that the program actually handles at the edges
It's not an opinion given to me by chatgpt.
The value proposition is that if you keep the information-oriented nature of clojure and add a little safety by default, isn't that better?
Maybe it could take another form, but hard to believe there doesn't exist a stricter set of defaults that might be worth it
For example, we have warn-on-reflection and errors on calling missing vars, and we won't want to turn that off
That history of Clojure still sounds weirdly persuasive, but it's hard to reconcile with my own experiences. I did RDF work in clojure, too, and while it was a clear win over java, I'm not sure I've ever seen keywords used as context-free attributes in practice, or rarely if ever. Namespaced keywords specifically I think are a good example of tacking on information you 'own' to a map you didn't construct yourself, eg it came from JSON over the wire, thus would fit with my 'open' map usage, which matches Hickey's original intent. My point is the vast majority of real code has small, closed maps, and that could be decoupled conceptually from the actual data a program processes.
> A key question was: one map or two? There are two very distinct use cases for maps: potentially > large, homogeneous, collection-like maps, e.g., mapping everyone’s username to their site visit > count, or comparatively small heterogeneous information-maps, e.g., a bunch of things we know > about a particular user in a particular context, like the email address, password, userid etc. I wanted > only a single map literal syntax and library for maps.
So, I guess it was a real design concern to him, too
I did data engineering in ocaml for a while, too, and there were only a handful of actual hashmaps required to basically build an RDB entirely in memory before streaming it out. A lot of lists, a lot of structs/records, and a handful of push-based streams. It was fine.
Occasionally it would have been nice to have utility types for things like merging, but type systems have gotten better. Typescript has them, for example.
I guess I'm glad clojure exists and he didn't just decide to do F#
I'm looking at a large-ish clojure codebase right now and most of the namespaced keywords are just standalone labels, not 'attributes'
One interesting exception is we use them for ex-data
I believe that's what typescript does
It can track all assoc/dessoc on maps as they get passed around when its statistically possible. And when not you can declare an interface for it
Sounds similar to Typed Clojure
It infers similar things
I don't think so. I had asked that to Ambrose a while back. You can define maps at every function input/output with a specific type and such. But it cannot automatically follow a map as it moves around and determine what keys were possibly added/removed as it flows through.
And to be honest, now that I think about it. Not sure typescript can either.
Could build those with wrap maps, easy ;)
i'm looking for episode 4 of tim baldridge's video series on zippers. episodes 1-3 and 5-7 seem available on youtube, but i'm not having much luck finding the fourth episode. does anyone know if that can be viewed somewhere?
Oh drats. Those episodes used to be hosted on pivotshare and accessible for a modest fee. But pivotshare is no longer around.
Hmmm... maybe they are still available for a fee? From https://www.youtube.com/@ClojureProgrammingTutorials description: > A collection programming tutorials for Clojure, covering logic programming, transducers, core.async, program optimization, and many more topics. > > Q) These videos aren't available in my country! > A) Videos are also available via Dropbox and Google Drive: (payment options below) same price, billed once a month. Users get access to raw .mp4 files. This is a manual process, so expect a 1-2 day delay, but this method should work better for some users. If you don't mind the Youtube experience, using this site will most likely be more satisfactory > > Paypal: http://goo.gl/xgTq0j > Bitcoin: http://goo.gl/TUk79e > > Q) Why do you charge for videos? Isn't Youtube free? > A) Unfortunately, in order to get any sort of income from Youtube, videos must have a very high view count. Somewhere in the range of 100k views per video. Getting that sort of viewing of tutorials related to programming in any programming language would be hard. Alas, no. Those payment URLs now 404.
I do remember paying the pivotshare fee to access Tim's videos... I don't remember if episode 4 existed there!
Sadly, I don't think Tim is part of the Clojure community anymore. I'll fire off an email to him and see if he responds.
https://github.com/clj-commons/rewrite-clj/pull/412, so thanks for the heads up @sogaiu!