A less controversial alternative to potemkin?

The clojure library potemkin has an import-vars function that makes it so we can easily define vars in one namespace and expose them in another. The library works, but doesn’t support clojurescript.

An alternative I’m thinking about is adding metadata at the ‘wrapper’ location to indicate to tooling that there’s a different var we actually want to check for metadata. For example:

(ns impl)

(defn afn "return :a" [] :a)

(ns public-api
  (:require impl))

(def ^{:doc/see-instead #'impl/afn} afn impl/afn)

Tooling could check for the presence of :doc/see-instead, and show the docs of the referenced var instead. This would solve the problem for me. We could also clean it up with a macro:

(defmacro defwrapper [name var-name]
  `(def ~(with-meta name {:doc/see-instead (resolve var-name)}) ~var-name))

(defwrapper afn impl/afn)

clojure.repl/doc can be easily patched to support this. Cider and other tools could presumably make use of such meta data to help with getting docs for a var. Is this a good idea?

Since it already requires a change to tooling, IMO it would be better to make the tools handle stuff like (def my-var other-var) in a special way that automatically treats my-var as an alias to other-var.

Realistically speaking how many API methods are you going to have?

Many times when seeing code using import-vars (or custom macros with similar goals) it seems like the developer was willing to add a whole new library/macro only to save 10 lines of code.

Is it really the hurdle to write the wrapper yourself?

(defn afn
  "extra long docstring that doesn't need to be on impl/afn anymore"
  [foo bar]
  (impl/afn foo bar))

In cases where impl/afn is variadic you can even often move that variadic part to the API method and call a singular (simpler) impl/afn arity.

I’d even argue that by writing the API methods yourself you end up with a bigger incentive to not break that API. impl you can change whatever you like without the API methods changing with them? With the macro variant those changes automatically transfer.

I personally see no value in import-vars, and at least for CLJS is that the analyzer often doesn’t have all the var metadata available when rebinding this way. This means it may emit sub-optimal code for users of the API part. I have made examples on this site how’d address that, but its been a while and cannot find it.

You suggestion of see-instead seems weird, isn’t impl supposed to be the implementation that is “hidden” and not part of the public API? Why would a user need to look at implementation details?

3 Likes

I agree. As far as I’m concerned, a “less controversial alternative to Potemkin” is to just not do that (re-exporting public Vars from one namespace to another). Have a proper public API with real code in it that you actually maintain. All these “hacks” just impede the use of tools and editors and users who want to read some straightforward API code.

3 Likes

That would certainly be easier for users but I don’t see how something like clojure.repl/doc would know my-var is defined in that way. I’m sure more sophisticated tooling could manage that.

I think the main use case for import-vars is when the implementation var is already exactly what you want for the api, just in the wrong namespace. If a variadic api function could dispatch to fixed arity functions in the impl namespace then I agree import-vars isn’t a good choice.

My :see-instead idea is more about just exposing the docstring and arglists metadata from the implementation function, in the case that the api function is exactly the same as the implementation function. I prefer to have doc strings on as many vars as is practical, and I typically have them on my implementation functions, so I need to choose between either duplicating the doc string on the api function (and risking it getting out of sync) or moving it to the api function, which would mean there’s no longer any obvious path to the doc string for any code that directly calls the implementation function (like other code in the impl namespace).

I have a similar issue. On most of my projects I have used “partial” in all my db namespaces to prefix CRUD operations with the right name of the table before calling the core DB functions I keep in the core.clj. This has been great for general clarity; I can call (db.users/READ 123) and, a year later, augmented the core function to make valid calls like (db.users/READ 123 [:id :first_name]); since I was using partials I only needed to update the core function and my ns for each table got that added feature. The downside is that partials don’t keep any of the metadata, so my tooling can’t help me with docstrings or arguments, etc.

So, yeah, partial is like a special case of the shared apis in question, and loss of metadata seems like a sad trade-off for the total wins that we otherwise score with it.