clojure 2026-09-19

Design bikeshed: Say you had the ability to "freeze" a namespace so that no new vars may be added to it.

(freeze-ns *ns*)
Would that A. Also freeze every var in the namespace B. Not affect the vars in the namespace at all C. Also freeze every var, except those explicitly marked with some metadata. You can't add new mappings or alter the metadata on a frozen namespace, and you can't bind a new root value, or change dynamic-ness or alter the metadata on a frozen var. (I do not think this is the direction that I will ultimately go down, but I still want to think through the implications)

Explain to me why I might want any version of this feature? What do I gain? What do I lose?

This came out of my experiments a few posts up where the goal is "more strongly enforce boundaries between different parts of the codebase." So what you would get is certainty that if code calls my.ns/security-check the code that is executed will be the same as it was at time of freeze - that is, no bug or mischievousness can alter-var-root . You lose the ability to then use the REPL usefully in any namespace you freeze, so its not something one would do in dev.

The more principled version of this feature is "these namespaces are from lib A and these namespaces are from lib B. lib A can alter-var-root access private vars or anything it wants in namespaces it has, but it is restricted from darker magics against the code in lib B." But defining a new construct to serve as "a grouping of namespaces" is..fraught.

The impetus was the module system metabase uses - https://github.com/metabase/metabase/blob/master/.clj-kondo/config/modules/config.edn - where it is a bunch of linter rules for what namespaces are allowed to use code in other namespaces. "How could I make this exist at runtime and not just be static analysis?"

In that context "freezing a namespace" works to limit what other modules can do at runtime via a bug, but it doesn't account for legitimate usages inside a module (like how derive is doing alter-var-root on its own global hierarchy) - so I already kinda know its a dead end. I just want to sus out what the appropriate relationship would be between namespace access and var access

Hmm, Polylith has rules about how code can interact with other "bricks" and the tool enforces that, but within the context of Clojure allowing you to change stuff at runtimeโ€”as you indicate here. I have code that uses alter-var-root on library code to fix bugs, so I'd be annoyed if those libraries had frozen their namespaces ๐Ÿ˜

and metabase's module system is loosely inspired by polylith - its all down the static analysis "skill tree"

๐Ÿ’ก 2

well to draw the jvm module system parallel - you would be annoyed by a library changing a private var that you alter-var-root'ed. You would be very annoyed if a library required by a library required by a library did it, and you weren't the one alter-var-root -ing

โž• 1

so when you want to get at a private field of a class in Java now you need a command line flag. You still have the same power as the user, but libraries need to tell you they broke encapsulation.

i know the unit of compilation is a form so namespaces must be side-effecting but it's always felt bad to me that they're globally mutable

i think denying binding dynamic variables is too far for my taste lol

Does direct linking get you some of these guarantees?

my strawman idea rn is

(in-module demo)

(ns example
  {:export true})

(def abc 123)
And all the vars/etc. in the example namespace get associated with the demo module. If demo doesn't export the namespace (somehow) block access by other modules. Then if a var is private and the caller isn't in demo, also block access.

> Does direct linking get you some of these guarantees? Yeah, some of them. Part of this exercise is figuring out what restrictions would even be palatable and which are too anti-thetical to clojure to work

i think denying binding dynamic variables is too far for my taste lol
Yeah - i would consider a dynamic var (that isn't private and is in an exported namespace) part of the API surface. I mean restricting the ability to do (.setDynamic #'example/abc)

๐Ÿ‘ 1

Okay, I better understand what you're suggesting now. A module would have a set of nses: some would be exported as the module API essentially and the others would be completely private and inaccessible from outside the module. Not sure that freeze-ns matches that desire tho'?

it does not match that desire - it was an attempt to separate off some of what a module would need to do.

I can think of two ways to configure a module - statically and at runtime via the REPL.

If you do it statically (like a data_readers.clj type thing) that isn't great for REPL development, but it is what you want when consuming a library. you don't want to be able to just define a namespace first before requiring a lib to bypass everything. If you do it at runtime, something like the following, you do eventually need to freeze the set of namespaces contained within the module (otherwise another module can just start defining new namespaces to backdoor access) and you do need to make sure other modules can't define new vars/alter existing vars in the namespaces that are exported.

(in-module demo)

(module-requires 'demo 'clojure.spec)

(ns math)

(module-export 'demo 'math)

(defn add [x y] ...)

(ns math2)

(defn sub [x y] ...)
And then, at least on the JVM, if you want to make this robust you need to also be interacting with JVM modules. (Which isn't the worst thing in the world, they do function somewhat dynamically, but its deeper into classloading/the compiler than i've yet gone. Adding a volatile boolean to Namespace for isFrozen was easy and made me feel like I did something)

Motivating example going through my head

(jetty/start-server! #'handler)
Should the library that has jetty/start-server! be allowed to alter-var-root on #'handler? Should it be able to access #'handler if it is marked private or if it comes from an unexported namespace? I think the answer to the first is no, modules shouldn't be allowed to redefine the code of other modules. But I think the answer to the 2nd is yes. This is a common pattern for interactive development.

So how can we prevent the first, allow the 2nd, and prevent (#'impl.private/thing ...)

use a different language lol

that would be no fun - and I think it requires forking clojure anyways so definitionally the end result would be a different language (although ideally a superset)

๐Ÿ˜„ 1

I doubt I've ever used alter-var-root except mechanically as a consumer of libraries that do so, so probably I misunderstand the point of this exercise, but I am reminded of https://ocaml.org/docs/functors

Most people don't - in the same way most people don't do setAccessible(true). The point of the exercise is "can we make the static checks for modules exist at runtime too." The opining about vars comes from approaching that problem holistically (like, clojure 1.42..how would it do this?)

(and a bonus brainworm - how would you do it s.t. it is applicable to dialects as well? clj/cljs/cljc/...)

> can we make the static checks for modules exist at runtime too. Well, granted that Clojure does not have an ML type system, isn't that what functors (Chicken Scheme also has them) do? You get a module by applying a module to a sort of spec/function, and that (I think) upholds your condition that the vars are untouchable from outside

@bhlieberman93 I'm not following, can you give an example (in pseudo code if you need to)?

I'm certainly out of my depth here, but my understanding of functors is that the input module adheres to the interface of the functor, and the resulting module supplies its own contract, but that whatever bindings are exposed in the input module are basically "consumed" when it is passed to the functor. In https://wiki.call-cc.org/man/5/Modules#functors, the input module could very well export some other binding apart from multiply, but that var/binding will be invisible after the new module is created. In the context of Clojure, I have no idea what this would look like tho

I think I am barking up the wrong tree here, because a functor, while it does provide this kind of encapsulation you describe, goes farther by consuming the input vars as I say