clojurescript 2024-11-20

Protocols are not reified in CLJS. Does that mean that I cannot got any information for a protocol during runtime? If I try to print the value of a protocol during runtime. I get an opaque function object:

(defprotocol MyApi
  :extend-via-metadata true
  (my-inc [this a]))

(println  MyApi)
; returns #object[Function]
Can I do anything useful with this function? If the information is lost during runtime, that implies, that I have to use macros for code that tries to inspect a protocols, right?

I'd say you need a macro to extract the data for the protocol. so the thing that uses it doesn't technically need to be a macro.

but yes, the runtime doesn't retain any useful info for the protocols. it lives mostly in the analyzer data

thanks for the clarification πŸ™‚

say for the above being in a foo.bar namespace you can look at the analyzer data in [:cljs.analyzer/namespaces foo.bar :defs MyApi]

it'll have :protocol-symbol as true and :protocol-info map like this

:protocol-info {:methods {my-inc [[this a]]}}

basically only contains the signatures, also somewhat similar repeated in the :sigs map

not exactly sure this compares to the CLJ data for this

Cool. Where can I find the analyzer data?

thats the whole analyzer data structure

from

(defprotocol TestProtocol
  (some-fn [this foo]))
in the demo.browser namespace I have for testing

or pprinted if you prefer that

{:meta
 {:protocol-symbol true,
  :file "demo/browser.cljs",
  :end-column 26,
  :column 14,
  :line 29,
  :protocol-info {:methods {some-fn [[this foo]]}},
  :end-line 29,
  :sigs {:some-fn {:name some-fn, :arglists ([this foo]), :doc nil}},
  :jsdoc ("@interface")},
 :protocol-symbol true,
 :name demo.browser/TestProtocol,
 :file "demo/browser.cljs",
 :end-column 26,
 :column 1,
 :line 29,
 :protocol-info {:methods {some-fn [[this foo]]}},
 :info nil,
 :end-line 29,
 :tag any,
 :sigs {:some-fn {:name some-fn, :arglists ([this foo]), :doc nil}},
 :impls #{},
 :jsdoc ("@interface")}

the data lives in the cljs.env/*compiler* atom, which you can access in the macro

or cljs.analyzer.api/resolve should give it to you when going from the symbol

so (cljs.analyzer.api/resolve &env the-sym) for (defmacro get-it [the-sym] (cljs.analyzer.api/resolve &env the-sym)) and (get-it SomeProtocol)

cool. thanks for sharing these details πŸ™‚

If have another question: There is a 3rd party package, which contains a protocol in a .cljc file. I require that package and I want to print a method in that protocol. It prints nil Concretely, it is this module:

[griffin.test.contract.protocol :as p]
With this content
(ns griffin.test.contract.mock)

(defprotocol State
  (init-state [this i]
    "Update the state given a mock is starting with initial state i")
  (swap-state [this f]
    "Calls f, the method, with the current mock state, replacing with the return value. NOTE: f may be called multiple times so should be free of side effects."))
In cljs (println init-state) returns nil. If I define my own protocol and try to print a method, then I get #object[my-ns$my-method] . Why is the former nil? What am I missing?

cljs.user=> (require '[griffin.test.contract.mock :as mock])
nil
cljs.user=> (println mock/init-state)
#object[griffin$test$contract$mock$init_state]
nil

Did you execute that in a cljs repl?

Yes - you can see that by the cljs.user part in my code block.

I found the issue: Namespace griffin.test.contract.mock clashes with var griffin.test.contract/mock For some reason this works fine in clj, but it is shadowed in cljs.

What exactly are you doing? It sounds like you're stumbling across GIGO.

Trying to make griffin.test.contract work in CLJS πŸ™‚ . If you only do one (require '[griffin.test.contract.mock :as mock]) it seems to work and you get the mock/init-state fn-obj . As soon as you require the β€œcore/main api” namespace (require '[griffin.test.contract :as c]) mock/init-state is nil.

And then I get the warning, which I mentioned above, from shadow-cljs

Huh, what..? I've never seen it before. Interesting.

Right, makes sense. A namespace like a.b.c creates a JS object like {a: {b: {c: {... the-ns-contents ...}}}}. So a namespace like a.b with a var c would end up as {a: {b: {c: the-var}}}, which overwrites the ns contents. This is not something you can fix from outside of the library - it's something that has to be changed in the library itself. And it might potentially be a breaking change...

This was tricky to find. Now it makes sense. Thanks for your help πŸ™‚