Fork me on GitHub
#clojurescript
<
2020-07-30
>
Yehonathan Sharvit06:07:23

I need help with macros. I’d like to create a macro defnmy similar to `defn` but that creates a function whose name is transformed by a function supplied to the macro. Example of usage:

(defnmy FOO clojure.string/lower-case [x]
    (inc x))
It should create a function named `foo` I was able to implement it using `resolve`:
(defmacro defnmy [name fn args body]
  (let [n (symbol ((resolve fn) name))]
    `(defn ~n ~args ~body)))
I would like the macro to workd on self-hosted cljs. The problem is that in cljs resolve is a macro and it works only on non-namespaced symbols. Help appreciated

noisesmith17:07:24

> resolve is a macro and it only works on non-namespaced symbols

(ins)user=> (resolve ')
#'
(ins)user=> (map resolve '[+ - inc dec])
(#'clojure.core/+ #'clojure.core/- #'clojure.core/inc #'clojure.core/dec)
it is not a macro, and it works on both namespaced and non-namespaced symbols

noisesmith17:07:46

the problem is that cljs doesn't have real vars, and resolve only works on vars

Yehonathan Sharvit06:07:04

The context of my request is that I am trying to make this library https://github.com/clj-commons/camel-snake-kebab self-host compatible and it has this `defconversion` macro https://github.com/clj-commons/camel-snake-kebab/blob/master/src/camel_snake_kebab/internals/macros.clj#L21

kwladyka08:07:25

Did you find any disadvantage of using https://github.com/binaryage/chromex vs raw JavaScript? This is library for google chrome extension. I have no experience with this, but I need to write something and trying to decide if do this with cljs or maybe in this case with js.

mfikes15:07:07

@viebel I'd do it this way https://gist.github.com/mfikes/c45ce81705786a04e617a419941afcab and, for the self-hosted case copy enough of the Planck code that bottoms out in the use of eval to pull it off.

mfikes15:07:05

An issue might arise if the code referenced in the function itself needs to be loaded; then there would be asynchronous issues to cope with.

Yehonathan Sharvit16:07:44

@mfikes your gist is awesome! Thanks

Yehonathan Sharvit16:07:41

But it makes me asking myself what would be the meaning of making a lib like https://github.com/clj-commons/camel-snake-kebab self-host compatible. One option is to make the lib works fine with planck. In that case, I can use planck.repl/requiring-resolve But then the lib would not work on other self hosted env (lumo, klipse). Can you think of another option?

mfikes16:07:15

Copy enough of Planck’s implementation of requiring-resolve into the lib—it is pure ClojureScript that bottoms out on eval

Yehonathan Sharvit16:07:50

What state would I pass to cljs/eval as first arg?

mfikes16:07:16

It bottoms out in cljs.core/eval (not cljs.js/eval) 🙂

Yehonathan Sharvit16:07:53

@mfikes Do I need resolve from planck or I can use resolve from cljs?

mfikes16:07:49

Oh, yeah, you’d need to copy Plank’s. And that involves cljs.js/eval

Yehonathan Sharvit16:07:18

But I can replace cljs.js/eval by cljs.core/eval.Right?

mfikes16:07:04

Well, they’re not directly interchangeable if that’s what you mean

Yehonathan Sharvit16:07:37

No. I meant including code adaptation (not verbatim copy/paste)

Yehonathan Sharvit16:07:54

(defn ns-resolve
  [sym]
  (binding [ana/*cljs-warnings* (zipmap (keys ana/*cljs-warnings*) (repeat false))]
    (eval `(~'var ~sym))))

mfikes16:07:04

That should be cool

mfikes16:07:39

Perhaps that leaves open the question of which ns the eval occurs in...

Yehonathan Sharvit16:07:36

If the symbol is fully qualified, it should not matter. Right?

mfikes16:07:13

Yeah, for this use case it might be cool

Yehonathan Sharvit16:07:21

So what’s the point of requiring-resolve vs resolve?

Yehonathan Sharvit16:07:52

Does planck have a magic trick to run eval synchronously?

mfikes17:07:42

This all surrounds whether the namespace for the function symbol passed to defmy has been required or not. If not, then resolve is going to return nil. The default implementation of eval for self-hosted environments is assuming the result of the evaluation is available synchronously. (See https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/js.cljs#L1238) But if you evaluate a require form, then it is up to each self-hosted environment to implement that. In Planck's case require does indeed have a synchronous implementation.

mfikes17:07:41

(Actually it used to be the case that require needed to be directly implemented by a self-hosted environment, but at some point a macro was introduced, but nevertheless the root issue remains regarding whether cljs.js/*load-fn* is asynchronous.)

mfikes17:07:03

Broadly speaking, you want to write a macro that, upon macroexpansion, makes use of a function that may or may not have been loaded at that time. And in self-hosted ClojureScript, the namespace loading mechanism is fundamentally asynchronous.

mfikes17:07:22

If your use case knows a priori that the function symbols being passed to defmy are already loaded, then resolve should be sufficient for that use case.

mfikes17:07:10

No magic. wizard

mfikes17:07:11

^ In the above, resolve is your custom implementation, not cljs.core/resolve

Yehonathan Sharvit17:07:38

Wow! Thanks for this crystal clear explanation

mfikes18:07:44

@viebel If you assume the fn has been loaded maybe you can just dispense with all of the complexity and just use eval and var

(defmacro defnmy [name fn args & body]
  (let [n (symbol ((eval `(~'var ~fn)) (str name)))]
    `(defn ~n ~args [email protected])))

Yehonathan Sharvit18:07:43

I was just thinking about that @mfikes What about setting the cljs-warnings to false like in

(binding [ana/*cljs-warnings* (zipmap (keys ana/*cljs-warnings*) (repeat false))]
    (eval `(~'var ~sym)))

mfikes18:07:54

I wonder if there are actually any warnings you would want to suppress... the cool thing about avoiding that is that the definition above based on just eval and var also works in Clojure (and thus is perfectly fine as a macro for JVM ClojureScript)

Yehonathan Sharvit18:07:23

Is eval and var preferable over clojure.core/resolve for Clojure and JVM ClojureScript?

Yehonathan Sharvit18:07:49

Or at least not worse

mfikes18:07:29

That's a good question. You could use a reader conditional and use clojure.core/resolve if in :clj , but I guess you are pondering if there is a reason to do that. 🙂

Yehonathan Sharvit18:07:45

Yeah. That’s my question

ghadi18:07:16

is this the world's biggest yakshave to get camel-snake-kebab working in a self-hosted environment?

Yehonathan Sharvit18:07:27

is (resolve sym) (better than (eval ('var sym))` ?

mfikes18:07:29

The yak is very hairy 🙂

ghadi18:07:11

I haven't read all the scrollback, but that library doesn't seem to do something that is fundamentally incompatible with any environment

ghadi18:07:23

(as in: it's for converting cases of strings)

johanatan18:07:40

is there a better value for :args for a zero-arity function than (s/cat) ?

Yehonathan Sharvit18:07:26

@ghadi I am not familiar with the yakshave expression. What does it mean?

ghadi18:07:38

> Yak shaving refers to a task, that leads you to perform another related task and so on, and so on — all distracting you from your original goal. This is sometimes called “going down the rabbit hole.”

mfikes18:07:09

Sometimes the yak shave is so lengthy, a day has gone by, and you have forgotten what you were actually trying to do.

ghadi18:07:29

I imagine camel-snake-kebab can't be more than 40 lines of code

ghadi18:07:37

but I have often been wrong 🙂

mfikes18:07:13

One aspect that appears to be clouding things is the dynamic ability to pass and resolve arbitrary functions. But the functions that transform function names are limited in scope right?

mfikes18:07:13

Ahh, yeah, it feels like this "higher order" macro is nifty, but maybe is also causing the challenges in porting to self-hosted.

Yehonathan Sharvit18:07:13

When defconversion is used inside camel-snake-kebab, the scope is indeed limited.

Yehonathan Sharvit18:07:15

nifty or too nifty (i.e. abuse)?

mfikes18:07:56

Oh, sorry by "nifty", I mean "clever"

Yehonathan Sharvit18:07:11

This is what I thought

Yehonathan Sharvit18:07:24

My question: is it too clever?

Yehonathan Sharvit18:07:37

Beign too clever makes sometimes the code hard to decode

Yehonathan Sharvit18:07:46

@ghadi camel-snake-case has 162 lines of code

Yehonathan Sharvit18:07:47

-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
ClojureC                         6             31              8            162
-------------------------------------------------------------------------------
SUM:                             6             31              8            162
-------------------------------------------------------------------------------