This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-12-15
Channels
- # adventofcode (46)
- # announcements (3)
- # aws (7)
- # babashka (47)
- # beginners (86)
- # calva (40)
- # cider (8)
- # clj-kondo (22)
- # clojure (63)
- # clojure-europe (16)
- # clojure-hungary (3)
- # clojure-nl (1)
- # clojure-norway (46)
- # clojure-sweden (1)
- # clojure-uk (3)
- # clojuredesign-podcast (2)
- # conjure (4)
- # datalevin (1)
- # events (1)
- # fulcro (5)
- # graalvm (4)
- # honeysql (8)
- # hyperfiddle (15)
- # music (1)
- # off-topic (5)
- # pathom (7)
- # pedestal (1)
- # polylith (3)
- # portal (19)
- # quil (1)
- # re-frame (36)
- # releases (1)
- # specter (3)
- # sql (3)
- # timbre (11)
- # tools-deps (4)
- # xtdb (55)
It seems to me that making a custom version of defn
such that it accepts all of the same arguments as clojure.core/defn
is pretty hard. I figured I could write a macro that does alter-var-root
after defn
, but that won't work if direct linking is enabled, I think? Are there any other approaches I ought to look into, apart from just writing the code that covers all arg permutations of defn
by hand?
https://blog.klipse.tech/clojure/2019/03/08/spec-custom-defn.html#args-of-defn-macro something like this is one option, I suppose. :thinking_face:
What are you actually trying to do?
I am trying to make a version of defn
that wraps the function body such that that code will be instrumented. Basically wrapping it in an Elastic APM span. So for example, this:
(defnspan my-sum
[x y]
(+ x y))
Would expand to something like this:
(defn my-sum
[x y]
(with-apm-span [...]
(+ x y)))
Additionally, I'd like to pick up things from the var metadata to use inside the [...]
.
If you're up for dirty hacks, you can call defn
as a function with #'
: (#'defn nil nil 'f '[x])
. The result could then be transformed and returned from a macro.
I did something like this https://github.com/ivarref/defnlogged/blob/main/src/com/github/ivarref/defnlogged.clj#L160 a while ago.. (I'm not sure that matches everything though.) That code is basically taken from https://github.com/taoensso/tufte/blob/master/src/taoensso/tufte.cljc#L531 macro..
I think alter var root will also work with direct linking, it's a question of when the var is dereferenced, at compile time or run time. As long as no one has dereferenced it, why not bang away at it?
Hmm, good point. The alter-var-root
impl seems simplest to me. It'd look something like this:
(defmacro defnspan
[fname & body]
`(let [v# (defn ~fname ~@body)
m# (meta v#)
span-opts# (into {}
(comp
(filter (fn [[k# _v#]] (= (namespace k#) "apm")))
(map (fn [[k# v#]] [(-> k# name keyword) v#])))
m#)]
(alter-var-root v#
(fn [f#]
(fn ~fname [& args#]
(let [opts# (merge {:name (str *ns* "/" '~fname)} span-opts#)]
(with-apm-span [span# opts#]
(apply f# args#))))))
v#))
That will have some performance overhead, I think, but I'm not sure how much.You can get the argslist from the meta and emit instrumentation for every arity to avoid apply
Note that if the lower arities call the higher arities you'll instrument multiple times, so you may want to only instrument the max arity
there is also this from malli https://github.com/metosin/malli/blob/master/src/malli/experimental.cljc each arity is available here https://github.com/metosin/malli/blob/54b5cb80ca623246ee9fe0e83b54a1c631f40a71/src/malli/experimental.cljc#L48C69-L48C83
the syntax usage looks like https://github.com/metosin/malli/blob/master/docs/function-schemas.md#function-inline-schemas
we have something very similar for OpenTracing spans, @U4ZDX466T. It works great.
@UK0810AQ2 good call about the lower arities, I hadn't thought of that
What about cases where the argslist was set via metadata and not parsed from the function form?
the correct solution is to default to the maximum arity but to first check metadata of :instrument-arities
which will be a set of numbers, akin to :inline-arities
we're not messing with the :argslist in ours, just setting spans based on the name of the function, so we've avoided that issue, but it's worthwhile to think about
@UEENNMX0T something similar to that alter-var-root impl, you mean?
yeah, we do (skipping details cuz it's work code):
(alter-var-root! v#
(fn [f#]
(fn ~fname [& args#]
(let [span# (start-span! ...)]
(set-data-on-span! span# ...)
(try (apply f# args#)
(catch ...)
(finally (end-span! span#)))))))
i surveyed our code and we do use this with multiple arity functions, but start-span!
and set-data-on-span!
and end-span!
are all idempotent
I think I might just do what partial
does and write out the arities by hand until four args and if there's more, do (apply f args#)
.
@UEENNMX0T just to clarify: do you use direct linking in prod?
@U4ZDX466T there is this
Thanks! That looks very nice and clean. I'm not sure I can take a dep here, but I'll look into it.
You can use malli to parse the data structures. e.g. This accepts a superset of the usual defn syntax: https://github.com/metabase/metabase/blob/master/src/metabase/util/malli/fn.clj#L58-L84
Eh, looks like things break with direct linking even before getting to alter-var-root!
... given:
(defmacro my-defn
[fname & body]
`(defn ~fname ~@body))
Then:
(binding [*compiler-options* {:direct-linking true}] (compile 'my.ns))
And:
clj -Sdeps '{:deps {co.elastic.apm/apm-agent-api {:mvn/version "1.38.0"}} :aliases {:x {:extra-paths ["classes"]}}}' -A:x
This happens:
user=> (my-defn f [x] (inc x))
Unexpected error (ClassNotFoundException) macroexpanding my-defn at (REPL:1:1).
clojure.core$seq__5479
Never mind, it works after all. I was compiling on Clojure 1.12.0-alpha5 and running on Clojure 1.11.0.
are there any tap>
idioms that you use at your companies/projects? i.e., is there some sort of extra meta-data you always pass along in taps? some common wrapper you like to use?
Two things:
• Wrap everything in a vector with extra data that helps you see which tap that was, e.g. (tap> [:doing-stuff data])
instead of just (tap> data)
• Whatever metadata Portal supports
Similar to ☝️, but I usually use wrapper maps instead of vectors, so I can include arbitrary context. Could, of course, use meta for that instead, but the maps are quick and dirty.
Both of those make a lot of sense! I'm currently going with the map approach but it's very ad hoc, usually in the format
{:event ::event-name
:data x}
i should write some kind of wrapper though, maybe with timestamps too or somethingYou certainly could. You could write a macro that included line number and other data if you wanted, but I tend to find that ad hoc works best for my workflow.
Usually I use a vector as above, but otherwise if it is just quick one: here is an idiom I learned here from Sean:
(-> (range 20)
(doto tap>)
(->> (map inc)))
This is a quick way to inspect something if it in a thread (`->`).I have a Joyride script that will either wrap a form with (doto .. tap>)
or add (doto tap>)
into a ->
pipeline: https://github.com/seancorfield/vscode-calva-setup/blob/develop/joyride/scripts/tap.cljs
(and I have that bound to ctrl+alt+d ctrl+alt+t
-- Doto Tap 🙂 )
My custom REPL snippets for Calva are full of tap>
calls: https://github.com/seancorfield/vscode-calva-setup/blob/develop/calva/config.edn
browsing through these snippets now : )
If you are a portal user, there is https://github.com/djblue/portal/blob/master/src/portal/console.cljc which will tap a value with its context and return it. It will also tap and re-throw exceptions and works with portals goto facilities 👌 also works for clj/cljs/cljr.
wrapping stuff in (portal.console/debug …)
is a pretty regular thing for me. It’s great.
One nit: Have you thought about adding reader macros?
I currently use playback but I don’t like how it displays values in the portal console
I have a bunch of helper functions to tap> values in threading macros and also a namespace to help me decipher errors coming from Postgres (most likely caused by me messing up HoneySQL syntax ;-)) https://github.com/lukaszkorecki/rumble/blob/1194ba15f1867384e1d67befa58fb30664154441/src/r/sql.clj#L68-L73
@U02EMBDU2JU Maybe the with-logging
stuff in next.jdbc
would help you there?
wrong person ;-) but.. I was not aware of with-logging :man-facepalming: that said, I'm pulling this pure-java SQL syntax formatter and that looks great in Portal :-)
When debugging helix components, I like to do stuff like
(defnc component-to-debug [props ref]
(tap> {:id `component-to-debug ...})
...)
then inspect values with the Shadow CLJS inspector on localhost:9630