The problem: function-a
takes the same
arguments as function-b
. In fact, function-a
calls function-b
. Without too much synchronized updating of
the function signatures, I want (doc function-a)
to show me
the same argument lists as (doc function-b)
.
The solution: Use :arglists
in the
metadata of the function.
(defn function-b [{:keys [a b c]}]
(println a b c))
(defn function-a
{:arglists '([{:keys [a b c]}])}
[args]
(function-b args))
That’s the TL;DR. Read on for some rationale, and for some nerdy diving into the worlds of static and dynamic analysis.
I had a case where I had several “pairs” of functions that shared their interface. The functions return SVG and the interface is a configuration map. One of the functions in a pair returns Hiccup, because that’s what I need for the components rendering the SVG. The other returns the rendered string, because that’s what I need for a JavaScript library I use that also renders the SVG.
Here’s a contrived example with the hiccup-producing part of the
pair: traffic-light-symbol-hiccup
:
(ns nrepl-lsp-arglists.svg-example
(:require [hiccup2.core :as h]))
(def kind->color {:stop :red
:wait :yellow
:go :green})
(defn- styled-circle [{:keys [size style]}]
[:svg {:width size
:height size
:viewBox [0 0 512 512]
:fill :none
:xmlns "http://www.w3.org/2000/svg"}
[:circle (merge {:cx 256
:cy 256
:r 200}
style)]])
(defn traffic-light-symbol-hiccup
"Returns SVG for a traffic light symbol as an SVG circle.
Args:
* size: The size (radius) of the circle.
* kind: The kind of the traffic light, one of :stop, :wait, :go.
* bells?: Whether the circle should have bells.
* whistles?: Whether the circle should have whistles."
[{:keys [size kind bells? whistles?]}]
(styled-circle {:size size
:style (merge {:fill (kind->color kind :black)
:stroke :darkgrey}
(when-not bells? {:fill-opacity 0.25})
(when whistles? {:stroke-width 5
:stroke-dasharray [10 10]}))}))
Now, the string producing function is as simple as:
(defn traffic-light-symbol-string [props]
(h/html (traffic-light-symbol-hiccup props)))
The problem with this is that my Clojure development environment (Calva) will not be very helpful with
looking up information about the
traffic-light-symbol-string
function. No docs and the
argument list of props
will be displayed.
Calva also has parameter hints that I can pop up while typing out the
params, and they will also simply say props
.
Well, sure there are better alternatives. One a bit more verbose:
(defn traffic-light-symbol-string-2 [{:keys [size kind bells? whistles?]}]
(h/html (traffic-light-symbol-hiccup {:size size
:kind kind
:bells? bells?
:whistles? whistles?})))
The bigger problem here is that I need to re-assemble the configuration map. That we can fix using:
(defn traffic-light-symbol-string-3 [{:keys [size kind bells? whistles?] :as props}]
(h/html (traffic-light-symbol-hiccup props)))
But now the linter (Calva is clj-kondo
powered) complains about unused variables keys
,
kind
, bells?
, and whistles?
.
Suppress unused-binding diagnostics to the rescue:
#_{:clj-kondo/ignore [:unused-binding]}
(defn traffic-light-symbol-string-3 [{:keys [size kind bells? whistles?] :as props}]
(h/html (traffic-light-symbol-hiccup props)))
However, in my real project some of the -string
functions have let
, and other local binding-boxes, and I do
want clj-kondo to help with those… So, I took this to the
Clojurians Slack and asked for advice which way to go, and I
also asked if there was some way I was missing.
There is a way.
I got help from several Clojure and Clojure tooling experts, so it turned into a super enlightening discussion for me. And there is icing on the cake! Let’s repeat it.
The solution: Use :arglists
in the
metadata of the function.
(defn traffic-light-symbol-string-arglists
{:arglists '[[{:keys [size type bells? whistles?]}]]}
[props]
(h/html (traffic-light-symbol-hiccup props)))
Calva has two providers of help with the documentation and arguments
list of functions: nREPL and clojure-lsp. nREPL is dynamic
and will tell me the truth about the argument list from what is
evaluated/compiled into the running program. clojure-lsp is static and
will tell me the truth about the argument list from what is typed into
the file. Both of these providers honor the :arglists
metadata.
In most development sessions (at least for me) both clojure-lsp and nREPL are running providing power to Calva. If the function is compiled into the program, nREPL’s answer will be used, otherwise clojure-lsp’s answer. In practice there is only a short period of time between the function text being updated and the code being compiled when I work, so both answers will be the same when I need to look them up.
Caveat. You may be tempted to provide the metadata like so:
(defn ^{:arglists '[[{:keys [size type bells? whistles?]}]]}
traffic-light-symbol-string-arglists
[props]
(h/html (traffic-light-symbol-hiccup props)))
This should be the same, and for clojure-lsp it doesn’t matter.
However, the Clojure compiler will overwrite the :arglists
entry of the metadata, or rather remove it, since it is not present in
the attr-map
. So doing it like that will render the nREPL
answer the less useful [props]
again (and Calva will not
know that this has happened and choose this more boring answer). Now you
know. I recommend you check that Slack thread out!
But what about the doc string? I have to admit I haven’t asked the experts about this yet, but here is what I currently do:
(defn traffic-light-symbol-string-arglists-w-doc
"Returns a string for a traffic light symbol as an SVG circle.
See `traffic-light-symbol` for more details."
{:arglists '[[{:keys [size type bells? whistles?]}]]
:doc (:doc (meta #'traffic-light-symbol-hiccup))}
[props]
(h/html (traffic-light-symbol-hiccup props)))
The reason I have both a regular doc string and a :doc
entry in the attr-map
is that the attr map is populated
dynamically, so only nREPL will see it. The shorter docstring is a
backup for clojure-lsp to show:
When the function is compiled, nREPL (and thus Calva) will provide the following answer::
I can only conclude my learnings from this inquiry with: The
attr-map
FTW!
Addendum. Wouldn’t it be nice if the REPL-version worked in
clojure-lsp too? And wouldn’t it be nice if it worked for both the
argument lists and for the doc string? Like so:
(defn function-b
"Prints `:a`, `:b`, and `:c` from the argment map.
* :a The a of the map
* :b The b of the map
* :c The c of the map"
[{:keys [a b c]}]
(println a b c))
(defn function-a
{:arglists (:arglists (meta #'function-b))
:doc (:doc (meta #'function-b))}
[args]
(function-b args))
There’s an issue for that! https://github.com/clojure-lsp/clojure-lsp/issues/1811