@cgrand I wish that I would understand better the mecahnics of upgradeable repl. I'm wondering if these are related, for example in inf-clojure
which I use with lumo, I've been constantly trying to make some loops I have running use process.nextTick()
to be appended in front of the js eventqueue. Long story short, every evaluation of get-arglists and get-completions, needs to be read and cycle trough many functions before evaluated, causing the event-queue of the single thread to fill up (and seems that setImmediate
and process.nextTick()
become meaningless in those cases). An alternative, which if I understand you with the concept of upgradeable repl, would be that functions like get-completions and get-arglists, could potentially not be read, but just become a function call from the socket repl, where the socket repl could have logic from a front end to call a compiled function or send it trough the reader (evaluate-text etc)?
(unrelated), you mention a callback before printing, how would the user not specify a special callback, how would a front-end "take over" a repl. Via runtime variable?
@hlolli thanks for your interest, first let me define what I mean by REPL: “read eval print loop” and nothing else. get-completion
for example don’t belong to the REPL but to the client (for lumo since the client and the REPL share the same process it’s blurry). Try the socket repl of lumo to get a “pure” repl.
@cgrand Yes I agree, it shulould not belong to the repl therefore I think this part here is not very cool https://github.com/clojure-emacs/inf-clojure/blob/master/inf-clojure.el#L828-L831
Fault of the front end inf-clojure more than lumo I guess.
If you start a plain clojure repl (eg with clojure
), you can do
~$ clojure
Clojure 1.9.0
user=> (loop [] (let [x (read)] (when-not (= :bye x) (println “you said” x) (recur))))
hello
you said hello
world
you said world
help I’m trapped!
you said help
you said I’m
you said trapped!
:bye
nil
user=>
here the code being evaluated takes control of the repl streams/process
ahh ok! suddenly I understand your point much better
It’s the same meaning of upgrade as in “HTTP 101”: before it was http now it’s something else
and this is what unrepl exploits to get you a better repl experience starting from a plain repl
In clojure this works because everything blocks
ok
have you considered async/await, would it fit here?
async/await is just js sugar, no?
it's a sugar for promises, but really blocks the reset of the body until returned, given the whole function is noted with async.
it doesn’t really block, it transforms in CPS (or similar) the body of async fns
in cljsjs my best proposal so far is to have the expression evaluates to a SuspensionRequest
value (which is just a special delay)
yes I guess so, it would be more akin to CPS. If you need real block, then I guess it wont cut the butter.
I don’t want real block, because JS is async, I just want to get the same feature at the end: the possibility for an expression to take over the repl. I don’t care if the expression is different than in Clojure.
I could see a new function for this in lumo, a callback is provided and starts a read-eval-print-loop
lunchcall, afk
ok I’ll let you some things to read then 🙂
(again it’s doable I’ve made three or four protototypes) what I want to find now is how to minimally modify lumo built-in repl to give it this power.
For a long time the design was variations over “set a callback using a public fn or global object”. I was never happy with that because it’s a callback setting that has an additional side-effect: suspend the main loop and then you get a lot of special cases: - what should I do of the return value (or the exception thrown) of the whole expression? - what if the user sets the callback twice? can’t start two loops, so: first wins? last wins? exception? - what if the user sets the callback from a really async callback? Should not stop the loop obviously, throw an exception and hope the exception will get properly reported at some point?
So it makes the contract “not great” and the implementation is more complex (because more checks).
That’s how I ended with the idea of a suspension request
The expression must evaluate to a suspension request so it provides easy answers to all the points above because it’s just a value (containing a function).
When the lumo repl gets a suspension request out of evaluation then instead of printing it and “looping”, it should call the function in the suspension request, and pass it the input stream and a resume-cb
.
the resume-cb
will resumes the lumo REPL.
to avoid adherence to node.js I introduce a small AsyncReader
protocol for the input stream passed to the suspension request.
EOM
note: lumo R-E-P-L is more R.on(EP) (with E and P fused together)
I wonder if a "user" would ever work with upgradeable repl vs front-end dev tools developers. But I wouldn't know. Sounds cool this contract of suspension request, going to check it out.
As a typical user, when I wanted to take over the repl, I found myself mixing core.async and the module readline https://github.com/hlolli/lumo-quiz-game/blob/master/src/lumo_quiz_game/main.cljs#L75-L78
and I misunderstood you probably for cljsjs as the cljsjs js for cljs libraries.
sorry I should add a keyboard shortcut to replace cljsjs by “self-host cljs”
unless I’m ocnfused about you ran it you didn’t take over the repl, you wrote a term app
That's right, I wonder what would happen if I add eval into this loop 🙂
but yeah, it doesn't avoid lumo's print and eval combo
nevermind
Not how I ment it, the callback function that is to be passed to a repl, is only set once and that callback function is written by front-end devtools developers. The users would then just start codeing without worrying about the underlying structure. So if for example unrepl has different printer than default repl, then that printer and all boilerplate will be passed to that function? ex. (lumo.repl/upgdrage-repl (fn [line] ..boilerplate with out-writer..)) where upgrade repl starts a loop.
So my question is, would a user ever have to worry about setting two callbacks or give a repl async function?
or rather, the callback-fn would have to be responsible for looping...
so the question is upgdrage-repl!
vs suspension-request
?
allow me to take a step back again, if the repl receives expression before the old return value is returned, do we wan't to add it into a queue, or print the return value asynchronously, possibly in wrong order (if that's even possible, I doubt actually)
In both case its’ a 1-fn API which takes the same callback as argument. I prefer suspension-request because (being value-based) it has clearer semantics and less room for error or abuse.
ok, I guess the suspension-request method would require a little tweak on the current execute-text function?
Maybe the best thing, is to do a PR and then it would probably spark a conversation on github and we could try it out etc. I think there are many that would like to see this feature.
both approaches require tweaks of this function (and/or its callers).
You are giving me an idea but I don’t know if it’s fully baked yet and, if it is, if it’s unholly
the idea would be to use both approaches: suspension-request
as the api and upgrade-repl!
as the implementation. This would require to let most of the caller chain unchanged at the expense of yet another piece of mutable state...
I was overthinking based on your question "what if the user sets the callback twice? can’t start two loops, so: first wins? last wins? exception?", but js is built on event-queue, so first in first out. Probably we are misunderstanding each other 🙂
(given that a callback is not sent to webworker)
> I was overthinking based on your question “what if the user sets the callback twice? can’t start two loops, so: first wins? last wins? exception?“, My pondering was mostly that a side-effectful api open room for undefined behaviors (from the contract point of view) which the user (even it’s a frontend dev) may have a hard time to spot as bugs at first.
Q: Your :thumbsup: is about using state or not using it?
I was reading again your messages, didn't find where you mentioned where a mutateable state is needed. I'm therefore neutral, I can see a way of one function api where the old loop is only suspended and new loop is takeing over. My thumbs up ment, you have theory on how to solve it and I'd like to see it implemented and try it out. If someone is really the final call here is antonio.
mutable state: if I don’t want to change the signature of the call-chain to execute-text then I need to set some mutable flag — pretty sure it would incur technical debt
How about adding callback to
(defn- execute-text
[source {:keys [expression? print-nil-result? filename session-id] :as opts}]
result-writer-cb
just thinking out loud like before.
That could be passed into eval part of repl to control the return value logic?that’s my plan
good plan, won't change the signature in a breaking way since it's destructured keys.
Is there a way to test a change to lumo without rebuilding?
@cgrand you can use the development workflow
boot dev
to watch & build the JS + CLJS
yarn dev
to launch it in Node.js without having to build the C++
Thanks!