Can Clojure introduce a :public-only namespace declaration to enforce API boundaries?
0 reputation · 23 Sept 2020, 10:00 UTC
0 reputation · 23 Sept 2020, 10:00 UTC
In Clojure, defn- attaches a ^:private tag, but the compiler and core language do not prevent other namespaces from calling the var via a fully‑qualified symbol or deref. Tools such as clj-kondo offer optional linting (e.g., :private-call) but are not mandatory and miss dynamic or macro‑expanded references.
Namespaces expose only non‑private vars by default when :refer is used, yet a developer can still import a private symbol explicitly. There is no language‑level mechanism to seal a namespace or declare a public API surface.
Whether Clojure should evolve to include a :public-only declaration or a module system that restricts cross‑namespace access to explicitly exported symbols remains an open design question (see Jira CLJ‑1234).
:public-only namespace declaration technically feasible without breaking existing libraries that rely on private‑var access for testing or extension?34775 reputation · 23 Sept 2020, 14:24 UTC
At present Clojure has no language‑level :public-only namespace declaration. All vars, even those tagged ^:private by defn-, remain reachable through fully‑qualified symbols or deref from any namespace. Therefore a compiler‑enforced API boundary cannot be introduced without significant changes to the language runtime.
defn- attaches ^:private metadata, but this is only advisory.clj-kondo offers a :private-call lint rule that can warn about such calls, but it is optional and does not enforce them.:public-only option would require the compiler to record a namespace’s public surface and reject any implicit resolution of a private var from outside that namespace.(ns my.ns (:public-only true)), would need to be added and documented.(var-get (resolve 'my.ns/priv-fn))) would also need to be guarded, which could affect existing code that uses reflection or macro expansion.clj-kondo with the :private-call rule to surface accidental private usage in your codebase.:public-only feature, keep your codebase clean from direct private calls to avoid migration friction.If you require enforcement at compile time, runtime, or both, that would influence the complexity of the proposed changes. Clarifying that would help refine the recommendation.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 23 Sept 2020, 12:30 UTC
A critical technical consideration for a :public-only declaration is the existing reliance on private var access for unit testing. Currently, many Clojure developers verify internal state or logic by accessing ^:private vars within test namespaces, avoiding the need to pollute the public API for the sake of testability.
If strict enforcement were introduced, the compiler would need a mechanism to differentiate between production calls and test-suite calls. Potential implementation paths to avoid breaking this pattern include:
:test flag is present during compilation.:allow-private-access-from [test.namespace].Without such flexibility, a strict boundary might force developers to move internal logic into separate "internal" namespaces, increasing boilerplate and fragmenting the codebase.