Welcome! Please see the About page for a little more info on how this works.

0 votes
in ClojureScript by

For context, started on Slack at https://clojurians.slack.com/archives/C03S1L9DN/p1650480331191719

This code works just fine on CLJ but fails with StackOverflowError on CLJS:

(def primes (remove
             (fn [x]
               (some #(zero? (mod x %)) primes))
             (iterate inc 2)))

The vital difference between CLJ and CLJS in this case is that in the former, lazy-seq ends up wrapping its whole body in a function with ^:once.

The reasoning why it works that way is because in the code above, the inner primes is a closed over value, it becomes a local. When the body fn of a lazy-seq (the one with ^:once) is called the second time, its locals are nil, so that inner primes is also nil. And iteration stops.
In CLJS, there's no :once and no locals clearing, so the inner primes is never nil, and the execution keeps on going into that fn without even calling that zero?.

Even if the reasoning doesn't sound plausible, the hypothesis that it's indeed :once that's to blame for the difference can be easily confirmed by implementing lazy-seq in CLJ without :once and noticing that it leads to StackOverflowError.

It's unclear to me what to do in this case, but perhaps at least a note on the https://clojurescript.org/about/differences page should be made about it.

1 Answer

0 votes

The blog post linked in the slack discussion is just incorrect about how this works.

I cannot speak to the cljs side of things, but on the clj side primes is never closed over in this expression, names that refer to vars (globals) don't have their values closed over.

primes always refers to the lazy being generated, never to nil. The reason this works is due to the short circuiting nature of some, and the fact that any potential divisors for a number will always comes before the number in the sequence.


How can the change of behavior when omitting `:once` be explained then?
So, I decided to dig deeper and wrote both `:once true` and `:once false` versions, compiled them, decompiled them, turned it into readable and somewhat minimal Java code, and tested it to confirm that the behavior is the same. And the only difference between the two Java versions is that the `:once true` version clears `this.coll` on the class that represents that `fn*` passed down to `LazySeq` and the `:once false` version doesn't clear it (same is true for `pred` but I removed it to simplify the code since it doesn't affect anything).

If anyone wants to play with the code, where it is:
- Based on `:once true` - https://pastebin.com/H3ZbUK9m
- Based on `:once false` - https://pastebin.com/9FZLtsQm

As you said, with the compiled/decompiled code I now can clearly see that the `primes` var itself has nothing to do with anything. But the iteration is stopped when needed in the `:once true` version not because of the specifics of the algorithm, with the short-circuiting `some` (after all, it doesn't short-circuit on prime numbers - it exhausts the coll), but rather because that very instance of `FilterLazySeqFn` stops being iterable - its `coll` becomes `null`. The owning `LazySeq` at that point is in the limbo - the value has been produced but it hasn't been assigned to `this.sv` in `LazySeq.sval`, so iterating over that very same `LazySeq` that's bound to `primes` in CLJ won't produce anything.