Fork me on GitHub
#clojurescript
<
2019-03-30
>
zane00:03:05

It's not possible for a macro m to expand to include a call to a function f that itself contains use of m, correct? Because of the staging rules?

mfikes00:03:47

@zane If I understand correctly, that should be OK. Here is a concrete example: https://gist.github.com/mfikes/f4eda0abcf2f6ad60db64e94b8d47178

zane00:03:12

@U04VDQDDY Does that still work in the self-hosted context?

mfikes00:03:01

In order to compile f, it has to macroexpand the m inside it, which simply expands to a call to f.

mfikes00:03:36

After macroexpansion, it would look like

(defn f []
  (when (< 0.5 (rand))
    (foo.core/f)))

zane00:03:28

Hmm. So, when I do this in practice I'm encountering an issue.

mfikes00:03:59

Maybe provide a minimal repro / gist.

zane00:03:07

Yes. 😅

zane00:03:40

@U04VDQDDY I think I might be misunderstanding how self-hosting works. In order to make that self-hosting compatible do you need to make core.clj a .cljc file?

zane00:03:07

Okay, there's definitely something wrong with my mental model of how self-hosting works, then.

mfikes00:03:30

Self-hosted ClojureScript will happily load a .clj file with macros in it. (It will, of course, compile that .clj file with the ClojureScript compiler.)

zane00:03:04

So the .clj file has to be valid ClojureScript, but it doesn't need to be in a .cljc file?

mfikes00:03:12

The reason for this is so that regular ClojureScript libraries that use .clj files for macros will be compatible with self-hosted ClojureScript.

mfikes00:03:21

Right. It is odd, but true. 🙂

zane00:03:34

That's extremely helpful.

zane00:03:39

Thank you so much!

zane00:03:16

I see. I had wondered why :cljs wasn't an option for the map load-fn provides its callback.

mfikes00:03:22

The intent is to, if at all possible, and within reason, prevent self-hosted ClojureScript from being a "fork" of ClojureScript, needing completely different codebases, etc.

zane00:03:32

That makes sense!

mfikes00:03:16

Oh, yeah, in that case, the :clj in the return for load-fn really means "ClojureScript". 🤪

zane00:03:51

In the project that spawned this question the file with the macro is a .cljc file.

zane00:03:56

I suspect that's where I went wrong.

mfikes00:03:29

The example I gave above could have used a .cljc file instead of .clj and it would still work the same, FWIW.

zane00:03:18

Would it work with both the function and the macro in the same .cljc?

mfikes00:03:29

Ahh, you had to go there!

mfikes00:03:47

OK, I think so. Let me see if I can write a variant like that...

zane00:03:38

What I'm finding is that if another .cljs file :require-macros the combined .cljc namespace and calls the macro it triggers a runtime error about the function not being defined.

mfikes00:03:55

Here is a single-file example, not worrying too much about double-definitions, etc. that works https://gist.github.com/mfikes/5c42494d3174988efb5776434b831a1a

mfikes00:03:45

Extending that a little to see if I can repro what I think you are saying...

zane00:03:35

My core.cljc is identical to yours, just with minimal.core as the namespace.

mfikes00:03:12

I added a foo.bar which is a bit like your minimal.core to the gist above

mfikes00:03:38

In your example, nowhere do you require the runtime namespace though

mfikes00:03:52

I'd recommend using two files instead of putting it all in one. (Just my preference.) http://blog.fikesfarm.com/posts/2018-08-12-two-file-clojurescript-namespace-pattern.html

zane00:03:17

Sorry, this is the error:

$ clojure -m cljs.main -m minimal.main
WARNING: Use of undeclared Var minimal.core/f at line 6 main.cljs
Exception in thread "main" clojure.lang.ExceptionInfo: TypeError: Cannot read property 'f' of undefined

zane00:03:44

Yeah, I've read that blog post of yours. (Thank you!) I just didn't realize that I could use .clj files in the self-hosted context!

mfikes00:03:57

With that recommendation, minimal.core wouldn't do :require-macros. In fact, you end up never really using :require-macros except when a cljs file does that on its own macros namespace.

mfikes00:03:32

Anyway, regardless of that recommendation, the reason your code is failing is because your code never requires the runtime namespace which defines f.

zane00:03:59

If foo.bar :requires foo.core it works fine.

zane00:03:27

But I'm worried about the usability implications of that.

zane00:03:12

How should clients know that they have to require more than just the macro if the macro is all they're using?

zane00:03:16

See what I mean?

mfikes00:03:47

Right. Me too. The "Two-File" approach is extremely useable, from the consuming namespace perspective. It practically looks like Clojure where you just require the namespace and refer symbols from it, without regard to whether they are fns or macros.

zane00:03:16

I'll give that a shot!

mfikes00:03:22

I think the relevant bit from that post that addresses your usability concern is the paragraph: > Notice that in the above, we simply required the lib.core namespace and used the macro. We didn’t need to use :require-macros or be concerned about whether reverse-lookup is a macro or a function, or that it uses a runtime function from the lib.core namespace.

zane01:03:15

If we had used :require-macros would it have blown up?

mfikes01:03:54

Yeah, because you are essentially only loading the "macro half" of that namespace, if you will.

zane01:03:41

I see, I see.

mfikes01:03:05

The really nice thing about this pattern is you simply require the namespace, and use its vars. Simple.

zane01:03:33

Whereas with .cljc files you can't do that? You have to use :require-macros?

mfikes01:03:56

Any complexity surrounding whether vars are macros, whether two file are used, etc, is pushed onto the implementing namespace. (The client consuming namespace need not be concerned.)

zane01:03:29

Interesting.

mfikes01:03:53

Well, you could take the two-file pattern which involves .clj / .cljs and try to cram it into a one-file pattern that just uses .cljc

zane01:03:07

I mean, it need be concerned with using :require and not :require-macros, unfortunately, right?

mfikes01:03:10

But then you end up wanting Macrovich or Chivorcam.

zane01:03:39

I saw Macrovich, and was considering it. 😩

zane01:03:34

Hmm. As a client of macro namespaces how do I know when it's safe to :require-macros?

mfikes01:03:53

FWIW, the gist https://gist.github.com/mfikes/5c42494d3174988efb5776434b831a1a is essentially the two-file crammed down to a one-file. I commented out the :require-macros in the client foo.bar namespace and it works fine.

zane01:03:04

Roger that.

mfikes01:03:27

So, you are worried about the situation when you are using a macros namespace and you use it by doing a :require-macros on it, only to find out that the macros namespace expands to use some runtime functionality that was never required.

mfikes01:03:30

My answer: Use the two-file pattern, and just require the runtime namespace. The two-file pattern lets the implementing namespace take care of whatever it needs to have loaded at runtime, etc.

mfikes01:03:07

I guess, I'm arguing that the two-file pattern is an example pattern that makes it easy on clients.

zane01:03:12

But as a client I can't know whether a given library is using the two-file pattern without seeing its source, right?

zane01:03:29

I buy your argument that the two-file pattern makes life easy, for sure. 🙂

mfikes01:03:38

I didn't invent the two-file pattern 🙂 I just wrote a post about what I saw as a good practice in the ClojureScript's core libraries and other places.

zane01:03:49

I see, I see.

mfikes01:03:19

Right, indeed, as a client of any namespace, if that namespace chooses to try to do funky stuff, they have to document up the wazoo to explain to you how to use it.

zane01:03:24

Would you say that use of :require-macros is dangerous, then, if you don't know whether a library you want to use is is using the two-file pattern or not?

mfikes01:03:08

That's a good question...

zane01:03:38

I'm leaning that way, but I'm new to this.

zane01:03:16

To put that another way: Clients don't know whether a macro-providing namespace has both runtime and compile-time elements.

mfikes01:03:23

Two example libraries that I can think of that include in their documentation specifics regarding macros are test.check and the need for :include-macros true and Quil, which does the same.

zane01:03:08

Kind of a bummer that that's necessary.

mfikes01:03:25

core.async used to be a little funky to use, but that has since been straightened out over time and now you can use core.async in ClojureScript just like you would in Clojure.

mfikes01:03:35

If you do (doc ns) the two-file pattern is described in teh paragraph with the heading "Implicit macro loading"

zane01:03:10

It sounds like the best practice for clients in the absence of explicit advice is to try getting the macro symbol with :require first, maybe.

zane01:03:39

:thinking_face:

mfikes01:03:49

Yeah, cljs.test, cljs.spec, core.async work this way, nothing to explain.

mfikes01:03:35

(And as an aside, you can refer to them as clojure.test, clojure.spec, clojure.core.async, as well, which is nice.)

zane01:03:14

Good to know.

mfikes01:03:31

I suppose the truth is, no matter what namespace you run into, you might end up needing to read its source if it deviates from that pattern.

zane01:03:28

I don't fully understand yet when :require fails.

mfikes01:03:55

So, you have a valid concern with respect to the question of "How do I generally consume a ClojureScript namespace that might involve macros, if I don't know if it follows the two-file pattern?"

4
mfikes01:03:05

I don't have a good answer to that. I would hope that good popular libraries just figure this out somehow and write things this way, but that is probably unreasonable to expect. That's partly why I ended up writing that post, I suppose. I kept trying to spread the good word, and finally just put it into a post.

mfikes01:03:41

In some sense, this macro stuff reminds me a bit of C++. In C++ there are lots of things you can do, but over time, the dev community settled on using a subset of that, honed over time as being best practices. That subset is a bit more sane and easy to keep in your head, grok, etc. Same thing is true with macros in ClojureScript. I think a lot of people naturally want to shove both runtime and macros into a single cljc file because that seems to get you closer to Clojure. But, I've personally started to appreciate the two-file (implicit macro loading) pattern as just being simpler, even though things are spread across two files.

zane01:03:49

It sounds like the only situation where :require fails is when the macro is in a .clj file, and there is no corresponding .cljs file. Does that sound right @U04VDQDDY?

mfikes01:03:31

Yeah, you need to have at least a stub .cljs file to follow the two-file pattern.

zane01:03:24

So "Try :require first, fall back to :require-macros" should work as general advice. :thinking_face:

mfikes01:03:07

Yeah, if you are just trying to use a namespace that you are not familiar with, that sounds reasonable.

4
zane19:03:03

@U04VDQDDY If you want both f and m to be callable from both clj and cljs you kind of have to use a one-file self-requiring .cljc file, right?

mfikes19:03:22

Yeah, things can get really mind-bending when trying to target that. Macrovich might help you retain your sanity. https://github.com/cgrand/macrovich

4
zane19:03:51

Got it. Great! Just checking my understanding.

zane20:03:29

@U04VDQDDY I'm noticing something strange about the "crammed" .cljc you posted above.

zane20:03:50

In the self-hosting context the arguments to macro m are still evaluated.

zane20:03:56

Works fine in regular cljs.

zane20:03:18

(ns test.core
  #?(:cljs (:require-macros [test.core])))

(defmacro m [& _]
  `(test.core/f))

(defn f []
  (when (< 0.5 (rand))
    (test.core/m do not eval)))

zane20:03:53

I'm testing it with plk.

mfikes20:03:39

@zane When compiling the above namespace as a macros namespace under self-hosted ClojureScript, it is fundamentally first being compiled by the ClojureScript compiler, and the resulting macros can then be used by the compiler. But... that function f sitting there makes a call to m, and at that point (since m is sitting in the same compilation stage as f) m is essentially still a function as far as f is concerned. You can repro this directly in a JVM-based ClojureScript REPL:

cljs.user=> (defmacro m [& _])
#'cljs.user/m
cljs.user=> (m do not eval)
WARNING: Use of undeclared Var cljs.user/do at line 1 <cljs repl>
nil

zane20:03:30

That makes sense to me. Would I be right to conclude from that that it is not in possible for a macro m to expand to include a call to a function f that itself uses m if both m and f need to be accessible at both macro expansion time and runtime?

zane20:03:02

I feel like it must be impossible because in order to separate the compilation stages you'd have to create a require cycle.

mfikes21:03:26

Hmm. That could be true. The underlying rules are quite simple, but the consequences are myriad. 🙂 Much like the game of Go. My opinion: When you tread into this territory, it is just too hard to figure out.

mfikes21:03:07

(I prefer to just keep things simpler, especially when it comes to macros.) Not saying what you are trying to figure out won't work. It just takes more brain cycles to sort out than I have. 🙂

zane01:03:50

That makes sense. It's complexity I wish I could avoid(!) but unfortunately it's inherent in our domain.

zane01:03:25

For now we've just inlined the results of the macro call in the function that uses it.

zane01:03:51

In any case, thank you so, so much for your help, Mike! 🙇:skin-tone-2:

Harald Pusch12:03:09

Is there a way to use undefined for JS interop? I want to use Number.prototype.toLocaleString with undefined locale so the browser default is used...

Harald Pusch12:03:52

Nevermind, using :undefined (or any other keyword) does the trick

mfikes16:03:52

@harald.pusch FWIW, you can do (.toLocaleString 1234 js/undefined)

hjrnunes19:03:51

hi, can anyone pls point me to docs on :> usage? As in [:> :div props child0 child1] thanks!

lilactown19:03:41

I’m assuming you’re using Reagent

lilactown19:03:45

based on your question

lilactown19:03:05

FYI there’s also #reagent

hjrnunes19:03:20

@lilactown yes! perfect, thanx!