cljs-dev

ClojureScript compiler & std lib dev, https://clojurescript.org/community/dev
markw 2019-08-30T23:04:39.000400Z

Hi All - I have a question about the implementation of LazySeq, actually several questions, but I'll stick with the main one first: Since the lazy-seq macro expands into a LazySeq deftype (which wraps the body in a thunk) at macro-expand time, it is not until runtime afaict when first or rest is called on the LazySeq that the the body is evaluated. However, when it is evaluated, the recursive nature of the body results in another call to lazy-seq, but this is after compile / macro-expansion time, right? How does that work?

markw 2019-08-30T23:14:16.000600Z

Calling first or rest results in a call to seq on itself, which finally calls .sval. Inside the .sval call (and this is now runtime) the thunk bound to the deftype member fn is called resulting in a new call to the lazy-seq macro. It seems there is some interleaving of macroexpansion and runtime, or I'm misunderstanding the implementation.

2019-08-30T23:22:54.002Z

Take my comment with a big grain of salt, since I am much less familiar with ClojureScript than Clojure/Java, but at least in Clojure/Java when you do a defn containing one or more uses of macros, those macros are expanded while compiling the entire defn form, not at run time.

2019-08-30T23:23:58.002500Z

i.e. at run time, there should be no remaining occurrences of lazy-seq or any other macro call.

markw 2019-08-30T23:24:25.003Z

I agree w/ your second statement, not sure how the first related though

markw 2019-08-30T23:24:57.004Z

What's confusing me is that at compiletime, (lazy-seq some-body) gets macroexpanded into the LazySeq deftype

2019-08-30T23:25:03.004100Z

Part of your original statement was "resulting in a new call to the lazy-seq macro". I do not think that is true.

markw 2019-08-30T23:25:12.004300Z

e.g. (new cljs.core/LazySeq nil (fn [] ~@body) nil nil))`

markw 2019-08-30T23:25:20.004600Z

doh.. (new cljs.core/LazySeq nil (fn [] ~@body) nil nil))

markw 2019-08-30T23:25:49.005100Z

that's the macroexpansion right there

markw 2019-08-30T23:27:21.005700Z

so that's what you have at runtime (i think), right?

markw 2019-08-30T23:28:16.006700Z

and when you call any ISeq function on that, there is another call to lazy-seq that occurs, which appears to be at runtime

2019-08-30T23:29:25.007500Z

Every occurrence of a lazy-seq macro should be replaced by an expression like its body at compile time, yes, and what you pasted above is what I see in ClojureScript source code as its body.

2019-08-30T23:29:59.008Z

That body contains no occurrences of the lazy-seq macro, though, so how would lazy-seq be involved at run time?

markw 2019-08-30T23:32:11.008600Z

the body typically would return a recurisve call to a procedure which calls lazy-seq

markw 2019-08-30T23:32:26.009100Z

for example: `(defn my-iterate [f x] (lazy-seq (cons x (my-iterate f (f x)))))`

2019-08-30T23:32:40.009600Z

A procedure which, at compile time, had all of its occurrences of lazy-seq replaced with the body of lazy-seq, too.

2019-08-30T23:33:34.010300Z

You could write my-iterate without using lazy-seq at all, just using the body of lazy-seq yourself, and you should get the same effect as if you used lazy-seq

2019-08-30T23:35:24.011100Z

Unless I have messed something up in my copy and paste (possible), you could write my-iterate like this:

(defn my-iterate [f x]
  (new cljs.core/LazySeq
       nil (fn [] (cons x (my-iterate f (f x))))
       nil nil))

2019-08-30T23:36:02.011600Z

and that is what the compiler sees and actually compiles, after it has done macro invocation

markw 2019-08-30T23:36:50.011800Z

OK yes i'm with you

markw 2019-08-30T23:37:00.012100Z

so now we agree macroexpansion is over, and ew have a lazyseq type

markw 2019-08-30T23:37:06.012300Z

well.. LazySeq

2019-08-30T23:37:18.012800Z

right. Nothing named lazy-seq remains after macro expansion

markw 2019-08-30T23:37:23.013Z

oen of the fields of that LazySeq is fn which is bound to that body

markw 2019-08-30T23:37:37.013300Z

bound to (fn [] (cons x (my-iterate f (f x))))

2019-08-30T23:37:41.013500Z

yes

2019-08-30T23:37:57.014100Z

or slightly more precisely, the function that expression evaluates to.

markw 2019-08-30T23:37:58.014200Z

now we call first, rest, or anything else... and it evals that body

markw 2019-08-30T23:38:10.014500Z

it calls the function

2019-08-30T23:38:10.014600Z

it calls that already-compiled body.

markw 2019-08-30T23:38:15.014800Z

yes

markw 2019-08-30T23:38:48.015400Z

which returns (cons x (my-iterate f (f x)))

markw 2019-08-30T23:39:01.015800Z

and my-iterate expands into... a call to lazy-seq

2019-08-30T23:39:14.016100Z

there is no lazy-seq remaining anywhere at this point.

dpsutton 2019-08-30T23:39:32.016800Z

my-iterate has been compiled into javascript at this point. its not expanded

2019-08-30T23:39:35.017Z

there is the function that looks like what I pasted above.

markw 2019-08-30T23:39:38.017200Z

ok, what happens when (my-iterate f (f x))) is evaluated

2019-08-30T23:40:04.017500Z

It creates and returns a new LazySeq object.

markw 2019-08-30T23:40:17.017700Z

ahhhh

markw 2019-08-30T23:40:24.017900Z

ok I think I get what you're saying now

2019-08-30T23:41:05.018500Z

(after first evaluating the (f x) part of the expression)

markw 2019-08-30T23:41:15.018700Z

right

markw 2019-08-30T23:41:43.019Z

I think this is a nuance of macroexpansion i've not encountered...

2019-08-30T23:42:12.019500Z

Think of them as a code-writing convenience, and they are all gone by the time "the real compiler" sees the ClojureScript code.

2019-08-30T23:42:49.020600Z

not exactly accurate, since macro expansion is part of the compiler, but perhaps useful in thinking of the distinction of macro expansion time.

markw 2019-08-30T23:43:01.020900Z

I understand the concept of macroexpansion time and the fact that all macro calls are replaced with their expansions by the time runtime comes around

markw 2019-08-30T23:43:48.021600Z

this is odd because at runtime there is a call that uses some already expanded code.. ors omething

markw 2019-08-30T23:43:54.021900Z

need to think on this a bit

markw 2019-08-30T23:44:20.022700Z

the way I have been thinking about it is that any macros the compiler can see, in source text, get replaced... and then you're done and on to runtime

2019-08-30T23:44:21.022900Z

I completely understand if that plus recursion can throw one for a mental loop, though.

markw 2019-08-30T23:44:24.023Z

but that's not quite the case there

markw 2019-08-30T23:44:25.023200Z

here*

2019-08-30T23:45:30.023900Z

It seems to me to be the case here. Macros are expanded at compile time, the post-macroexpanded code is compiled to JavaScript, and then you are completely in the JavaScript runtime.

2019-08-30T23:47:33.025500Z

The post-macroexpanded code is a function that returns a new object. One field of that object happens to be a reference to an already-compiled function, in this case an anonymous one that happens to call the outer function.

markw 2019-08-30T23:48:29.025800Z

I've got it

markw 2019-08-30T23:48:37.026Z

I don't know why I was so confused

markw 2019-08-30T23:48:56.026600Z

the second call to my-iterate was already macroexpanded at that time

markw 2019-08-30T23:49:51.027100Z

Wow that was a bit of a headsplosion for me

markw 2019-08-30T23:50:00.027600Z

thanks for taking the time to patiently explain it

2019-08-30T23:50:25.028100Z

Cut yourself a little slack -- I think it has that effect on lots of people the first time they understand it (and many just use it without trying to pick it apart).

markw 2019-08-30T23:50:59.028700Z

there is always the draw over time to try and pick apart clojure/script source for me... hah

2019-08-30T23:51:13.029Z

It reminds me of the saying "In order to understand recursion, one must first understand recursion"