This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2021-08-18
Channels
- # announcements (5)
- # aws (14)
- # babashka (5)
- # beginners (39)
- # brompton (9)
- # chlorine-clover (10)
- # cider (2)
- # clj-kondo (107)
- # cljfx (2)
- # cljsrn (7)
- # clojure (40)
- # clojure-australia (2)
- # clojure-conj (5)
- # clojure-europe (11)
- # clojure-japan (2)
- # clojure-nl (3)
- # clojure-spec (1)
- # clojure-uk (6)
- # clojurescript (8)
- # cursive (20)
- # datahike (6)
- # degree9 (2)
- # deps-new (2)
- # development-containers (11)
- # fulcro (26)
- # jobs (1)
- # joker (1)
- # kaocha (1)
- # lambdaisland (1)
- # malli (6)
- # membrane (1)
- # nbb (1)
- # news-and-articles (2)
- # off-topic (3)
- # pedestal (23)
- # re-frame (19)
- # reagent (6)
- # sci (110)
- # shadow-cljs (7)
- # tools-deps (9)
- # xtdb (20)
@borkdude I created a minimal example here: https://github.com/victorb/sci-reagent-with-let, tried a bunch of different things suggested and also guessed but to no avail
You are not using your own macro fn here: https://github.com/victorb/sci-reagent-with-let/blob/965b3309ba9b571b8e90f2e817fd16e3ad612d11/src/main/starter/sci.cljs#L16
Yeah, sorry about that, I've tried wrapping :require'd reagent.core/with-let in a .clj macro and function, tried adding it directly via :bindings, via :namespaces and a bunch of other versions but none are working (with different errors) I'll try adding all the different things I've tried in the same repo
Can you try exactly what I posted above this thread? I gave two working examples. It might be good to start from there.
Added bunch of different tries now: https://github.com/victorb/sci-reagent-with-let/tree/44e937e6cd6c9d30d924533f16f97614067f2861/src/main/starter Looks something like this atm:
I will look at this closer tonight but with with-let1 the issue is that your fn expands into a call to reagent.core/with-let which you haven’t provided in your SCI config. You have to give an implementation of your macro, you can’t wrap a macro from the .clj code as this is not available at runtime
ok, I'm back now. let me just add the with-let macro to nbb, then you can just copy the config
Back here too now. "the issue is that your fn expands into a call to reagent.core/with-let" Hmm, yeah, that seems about right, gonna see if I can correct the SCI config
@UEJ5FMR6K ok, with-let now works with nbb 0.0.44 https://github.com/borkdude/nbb/blob/main/ink-demo.cljs
see the config here: https://github.com/borkdude/nbb/blob/main/src/nbb/reagent.cljs
Nice! I was close, but yours is way better : ) Just as Ithought then, just a user error. Got it all working now, thanks a lot for the help, really appreciate it!
Oh. Haven't gotten as far to integrate the PoC into the project itself yet, anything obviously broken?
Add reagent.ratom/reactive?
and synchronizing the reagent.ratom/*ratom-context*
properly, I don't think that currently works
I see. Thanks, there is no rush and you've helped plenty already. I'll give it a shot and see if I can get it to work, otherwise ask a bit more targeted questions
@UEJ5FMR6K Pushed the final fix, now finally
also properly works with with-let
Random note, just noticed SCI has 1024 commits just yesterday! Thanks for the awesome work on it borkydude, saved lots of time and always been a joy to use! 🎉
Kind of a follow up to yesterday’s discussion regarding macros; I’m trying to expose the clojure.repl/doc
macro in sci (ideally emitting output for the actual ClojureJVM symbol that’s referred to, not the sci-equivalent) and have gotten a bit stuck.
Here’s the closest I’ve gotten: (sci/eval-string "(doc println)" {:namespaces {'user {'doc (sci/copy-var clojure.repl/doc (sci/create-ns 'user nil))}}})
Which throws a ClassCastException from
.
Any tips?
@deactivateduser10790 babashka itself exposes clojure.repl/doc as well. if you want to enable printing then you can do (sci/alter-var-root sci/out (constantly *out*))
but if you want to look outside of the SCI ctx for what Clojure vars expose, then you could rewrite the clojure.repl/doc impl to do so.
Ah ok. I guess I’ll need to bind sci/out to a StringWriter though, as I need to send the output to somewhere other than local stdout.
@deactivateduser10790 you can also use sci/with-out-str
for this, if that works for your use case
Yeah correct. Basically I’m using sci as a repl within a chat server.
Right now I just dump the result of evaluating a chat message, but capturing stdout would also make sense.
Don’t laugh too hard at the code: https://github.com/pmonks/for-science 😉
It’s in the Clojure Discord server, if you’re interested in taking it for a spin: https://discord.gg/discljord
It’s the replacement, yes. The clojure one’s admin left without anointing a new admin, so that one gets spammed pretty regularly, and no one has the ability to block the spammers.
As a result the admins of discljord have requested that links to the old one be removed from other Clojure resources and replaced with the discljord one, with mixed success…
Back to my bot though - I implemented what you described above, approximately as follows:
(let [sw (java.io.StringWriter.)
result (print-str (sci/binding [sci/out sw
sci/err sw]
(sci/eval-string (str "(use 'clojure.repl)\n" code)))]
Where “code” is the content of the chat message typed by a human.
However when “code” is (doc println)
, the StringWriter is empty (and result is “nil”, as expected).It gets weirder too:
(println "hello, world!")
gives sw = “hello, world!“, result = nil (correct)
(range 10)
gives sw = “”, result = (0 1 2 3 4 5 6 7 8 9)
(correct)
(map println (range 10))
throws a ClassCastException (why???)
and
(doall (map println (range 10)))
works as expected.
Am I right in thinking that laziness “escapes” the call to the interpreter, but then fails for “I/O-ish” things, because the binding is no longer in effect? If so, is there any way to force de-laziness at that call site, short of wrapping “code” with a (doall)
?
I am - that’s what print-str
does. But that’s outside the sci/binding
so any laziness with side effects that rely on those bindings will fail.
Yeah exactly. But I’m still surprised / concerned that sci lets laziness “escape” the interpreter. That doesn’t seem right.
And of course this also doesn’t explain why doc
isn’t working (is not producing any output).
Should I just raise an issue on that in the repo?
about laziness: there isn't a single right answer there, sci has different use cases, protecting the user against laziness isn't something that should be a choice inside the lib imo
in earlier versions, sci had something like "realize-max" which threw if you realized a lazy seq with more than n elements, this was to protect you from evaluating (doall (range))
, but this came with performance trade-offs which in the end weren't really worth it
user=> (let [sw (java.io.StringWriter.)] (sci/binding [sci/out sw] (sci/eval-string "(clojure.repl/doc inc)")) (str sw))
"-------------------------\nclojure.core/inc\n([x])\n Returns a number one greater than num. Does not auto-promote\n longs, will throw on overflow. See also: inc'\n"
Conversely:
(let [sw (java.io.StringWriter.)] (sci/binding [sci/out sw] (sci/eval-string "(clojure.repl/doc println)")) (str sw))
""
(let [sw (java.io.StringWriter.)] (sci/binding [sci/out sw] (sci/eval-string "(clojure.repl/doc if)")) (str sw))
""
user=> (let [sw (java.io.StringWriter.)] (sci/binding [sci/out sw] (sci/eval-string "(clojure.repl/doc let)")) (str sw))
""
etc. etc.
All of which produce output when run with “real” Clojure.
This is with v0.2.6, which is the latest AFAIK?
hmm yeah, perhaps not, special forms, and println are special, they might not have the proper docstrings
Is master stable?
but you mentioned you were going to pick the docstrings from "real" Clojure anyway, if you're running in a JVM then this is easy.
Ok - I’ll use that. And regarding “Clojure doc” vs “sci doc”, I don’t have a strong preference. Whatever is less code for me to write & maintain. 😉
@deactivateduser10790 rough version:
user=> (let [sw (java.io.StringWriter.)] (sci/binding [sci/out sw] (sci/eval-string "(clojure.repl/doc println)" {:namespaces {'clojure.repl {'doc ^:sci/macro (fn [_ _ sym] (binding [*out* @sci/out] (clojure.repl/find-doc (re-pattern (str sym)))))}}})) (str sw))
Back to the earlier topic of laziness, it seems like that should be scoped to a sci context. In my case I’m not maintaining a context across calls to eval-string, so I would expect any interpreter “state” to go away as soon as that call is ended.
(and laziness, by virtue of the possibility of side-effects via bindings, I’d consider as part of that “state”)
(in effect I want a self-contained, one-shot interpreter)
well, as I said, that's just one use case. there are use cases where this escaping is desirable, e.g. when returning a function from the interpeter which should be callable outside of the context
And in those cases is it onerous for the consumer to maintain a context?
Given that that provides them with “long lived” scope?
About laziness escaping: I'm very caution of adding features around protecting users from this as this will vary from use case to use case. Write your own protection layer for your use case, for now.
I have tried protecting users from other stuff and eventually I removed it because it wasn't working 100% correctly or it degraded performance.
Right, and I’ve already done that, including limiting sci’s execution time (right now to 2 seconds max). I’m just suggesting that this is a violation of the principle of least surprise. Sci already has contexts (which I’d understood to be a form of interpreter scoping), and because my use case didn’t need that, I didn’t use them. Now I discover that some of sci’s “scope” is indeed escaping the interpreter context (which just happens to not work for side-effecty code).
Take it as general feedback of the “is this design consistent?” form, rather than feedback of the “I’ve found a bug!” form. 😉
I hope you haven’t misunderstood it as the latter - that wasn’t my intent. [edits because words are hard]
The idea of the context is mostly to remember config + side effects like defined vars/functions over multiple invocations
Right. Seems like “state of laziness” could be considered as part of the same thing.
(since it includes state that has to survive across calls to eval-string)
BTW I’m trying to find how clojail used to do this…
(it was what I’ve used in the past for this kind of thing…)
every call to eval-string or eval-string* has a context, but with eval-string* that context is explicit while with eval-string the context is created for you every time
Sure, at the implementation level. But at the conceptual level eval-string only has a context within that one call.
As a consumer I never need to know about it.
I just fire a string value at eval-string and (in theory) everything that string value does is encapsulated within that call - nothing leaks out.
(except the final result of evaluating the string, and any bindings, of course)
that was never the intent of the context. e.g. with malli and clj-kondo, it's quite the reverse because the returned functions are basically closures over the context which can be invoked many times
In which case those consumers can (and should) create a persistent context and keep it around, no?
Since they need to explicitly manage a long-lived scope?
Again, take this feedback as “is this design consistent?“, not “I’ve found a bug!” 😉
e.g. (eval-string "(def x 1) (fn [] x)")
, what is the principle of least surprise here when invoking the result of eval-string?
user=> (def x 1)
#'user/x
user=> (fn [] x)
#object[user$eval146$fn__147 0x1be59f28 "user$eval146$fn__147@1be59f28"]
Something like that
Though of course the result of the first line gets thrown away.
I meant, invoking the result, like (*1)
I think it's better to document various use cases of SCI and how to leverage it e.g. for your one shot interpeter use case than to make these choices inside the lib. So if you want to contribute to the docs, feel free to do so.
And, in my case, the var x
gets thrown away too.
Since I don’t want / need long-lived state, so haven’t provided a context.
Again, is this design consistent? That the “one shot interpreter” use case has to do extra work to meet its need? Work that isn’t necessarily if lazy results are de-lazied within eval-string, and by doing so other use cases (that make use of explicit long-lived contexts) are not impacted?
These are not rhetorical questions btw - there are different answers with varying trade-offs.
To answer my second question (the weird behaviour with map
and println
) - wrapping the call to sci/eval-string
in a doall
seems to have solved that issue, which confirms that laziness “escapes” sci.
Is that intended though? That doesn’t seem safe to me…