From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Tomas Hlavaty Newsgroups: gmane.emacs.devel Subject: Re: continuation passing in Emacs vs. JUST-THIS-ONE Date: Mon, 10 Apr 2023 23:47:16 +0200 Message-ID: <87leizif4r.fsf@logand.com> References: Mime-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="24377"; mail-complaints-to="usenet@ciao.gmane.io" Cc: Jim Porter , Karthik Chikmagalur , Thomas Koch , "emacs-devel@gnu.org" To: Stefan Monnier Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Mon Apr 10 23:48:27 2023 Return-path: Envelope-to: ged-emacs-devel@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1plzNC-00067F-AS for ged-emacs-devel@m.gmane-mx.org; Mon, 10 Apr 2023 23:48:26 +0200 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1plzMM-0001n0-Og; Mon, 10 Apr 2023 17:47:34 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1plzML-0001mp-5j for emacs-devel@gnu.org; Mon, 10 Apr 2023 17:47:33 -0400 Original-Received: from logand.com ([37.48.87.44]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1plzMI-0007gI-Kh for emacs-devel@gnu.org; Mon, 10 Apr 2023 17:47:32 -0400 Original-Received: by logand.com (Postfix, from userid 1001) id 2074F19E638; Mon, 10 Apr 2023 23:47:20 +0200 (CEST) X-Mailer: emacs 28.2 (via feedmail 11-beta-1 I) In-Reply-To: Received-SPF: pass client-ip=37.48.87.44; envelope-from=tom@logand.com; helo=logand.com X-Spam_score_int: -18 X-Spam_score: -1.9 X-Spam_bar: - X-Spam_report: (-1.9 / 5.0 requ) BAYES_00=-1.9, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Original-Sender: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.devel:305229 Archived-At: On Sun 02 Apr 2023 at 22:09, Stefan Monnier wrot= e: >> (defun await (future) >> (let (z) >> (while (eq 'EAGAIN (setq z (funcall future))) >> (accept-process-output) >> (sit-for 0.2)) >> z)) > > So `await` blocks Emacs. Not if I run the top-level await in a separate thread, e.g. (make-thread (lambda () (await ...))) Or in case of iter, await-in-background should work without any thread. It block Emacs because I want it to block emacs in these examples: I want to press M-e (bound to ee-eval-sexp-eol for me) and observe what it does. I.e. I want to see it working even though my Emacs UI seems blocked. That is useful for testing purposes. > IOW, your `await` is completely different from Javascript's `await`. It depends what do you mean exactly and why do you bring javascript as relevant here. Only as an implementation detail it is different. The user interface is the same and is better than futur.el I think. Of course, it is implemented in Emacs Lisp so it will not be the same. Javascript does not have asynchronous processes or threads. You probably want await-iter and async-iter which use cps like other langua= ges. Also Emacs does not have such sophisticated event loop like javascript. But from the user point of view it does the same thing. I would say that futur.el is nothing like what one can see in javascript or other languages. Even the user interface is completely different. >> (defun promise-pipelining-server3 (req) >> (funcall >> (byte-compile-sexp >> (let (f v z) >> (dolist (x req) >> (if (atom x) >> (push x z) >> (cl-destructuring-bind (k op l r) x >> (let ((ll (if (symbolp l) >> l >> (let ((lk (gensym))) >> (push `(,lk (slowly-thread ,l)) f) >> `(await ,lk)))) >> (rr (if (symbolp r) >> r >> (let ((rk (gensym))) >> (push `(,rk (slowly-thread ,r)) f) >> `(await ,rk))))) >> (push `(,k (,op ,ll ,rr)) v))))) >> `(lambda () >> (let ,(nreverse f) >> (let* ,(nreverse v) >> (list ,@(nreverse z))))))))) > > And the use `await` above means that your Emacs will block while waiting > for one result. `futur-let*` instead lets you compose async operations > without blocking Emacs, and thus works more like Javascript's `await`. Blocking the current thread for one result is fine, because all the futures already run in other threads in "background" so there is nothing else to do. Like thread-join also in futur.el. If you mean that you want to use the editor at the same time, just run the example in another thread. But then you have to look for the result in the *Message* buffer. If I actually want to get the same behaviour as C-x C-e (eval-last-sexp) then I want await to block Emacs; and this is what await at top-level does. Using emacs subprocesses instead of threads in promise-pipelining-server4 shows also nicely that the example spawns 7 emacs sub-processes that compute something in the background and await then collects the results in the right time as needed. >>> This blocks, so it's the equivalent of `futur-wait`. >>> I.e. it's the thing we'd ideally never use. >> I think that futur-wait (or wrapper future-get) aka await is essential >> but what futur.el provides is not sufficient. There need to be >> different await ways depending on use-case (process, thread, iter). > > Not sure what you mean by that. `futur-wait` does work in different ways > depending on whether it's waiting for a process, a thread, etc: it's > a generic function. Sure. But there are 3 cases and the 2 cases in futur.el do not work with iter (i.e. without asynchronous processes or threads). > The `iter` case (same for streams) is similar to process filters in that > it doesn't map directly to "futures". So we'll probably want to > supplement futures with "streams of futures" or something like that to > try and provide a convenient interface for generators, streams, process > filters and such. No, the iter case does map directly to futures: (await (async-iter (let ((a (async-iter (message "a1") (await-iter (sleep-iter3 3)) (message "a2") 1)) (b (async-iter (message "b1") (let ((c (async-iter (message "c1") (await-iter (sleep-iter3 3)) (message "c2") 2))) (message "b2") (+ 3 (await-iter c)))))) (+ (await-iter a) (await-iter b))))) ;; a1 ;; b1 ;; c1 <- a, b, c started in background ;; b2 ;; @@@ await: EAGAIN [15 times] <- 15 * 0.2sec =3D 3sec ;; a2 <- had to wait for the first sleep ;; c2 <- second sleep already computed in bg ;; @@@ await: 6 ;; 6 (#o6, #x6, ?\C-f) The difference with for example javascript is that I drive the polling loop explicitly here, while javascript queues the continuations in the event loop implicitly. >> await is necessary for waiting at top-level in any case. > > That's what `futur-wait` is for, indeed. > >> For top-level waiting in background, use await-in-background instead. > > `future-let*` seems to provide a better alternative that doesn't need to > use a busy-loop polling from a timer. The polling loop is needed for some use-cases (asynchronous processes and iter). Not for threads. In the case of async-thread, await collapses into thread-join and the polling loop "disappears" because async-thread future never returns EAGAIN. So I do not need an extra implementation for threads, because the existing case for asynchronous processes works without any change also with threads, just even more efficiently. >> Calling await immediately after async is useless (simply use blocking >> call). The point of future is to make the distance between those calls >> as big as possible so that the sum of times in the sequential case is >> replaced with max of times in the parallel case. > > You're looking for parallelism. I'm not. What do you mean exactly? I am asking because: https://wiki.haskell.org/Parallelism_vs._Concurrency Warning: Not all programmers agree on the meaning of the terms 'parallelism' and 'concurrency'. They may define them in different ways or do not distinguish them at all. :-) But it seems that you insist on composing promises sequentially: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises However, before you compose promises sequentially, consider if it's really necessary =E2=80=94 it's always better to run promises concurrent= ly so that they don't unnecessarily block each other unless one promise's execution depends on another's result. Also futur.el does seems to run callbacks synchronously: The above design is strongly discouraged because it leads to the so-called "state of Zalgo". In the context of designing asynchronous APIs, this means a callback is called synchronously in some cases but asynchronously in other cases, creating ambiguity for the caller. For further background, see the article Designing APIs for Asynchrony, where the term was first formally presented. This API design makes side effects hard to analyze: If you look at the Javascript event loop and how promises are scheduled, it seems rather complicated. If you use threads or asynchronous processes, there is no reason to limit yourself so that those do not run in parallel unless the topological ordering of the computation says so. If you use iter, then obviously it will not run in parallel. However, it can be so arranged that it appers so. Like in javascript for example. In this javascript example, a and b appear to run in parallel (shall I say concurrently?): function sleep(sec) { return new Promise(resolve =3D> { setTimeout(() =3D> {resolve(sec);}, sec * 1000); }); } async function test() { const a =3D sleep(9); const b =3D sleep(8); const z =3D await a + await b; console.log(z); } test(); Here the console log will show 17 after 9sec. It will not show 17 after 17sec. Can futur.el do that? Are you saying no? > I'm trying to provide a more convenient interface for async > programming, So far I am not convinced that futur.el provides a good interface for async programming. Flat unstructured assempler like list of instructions is not a good way to write code. async/await in other languages show nicer structured way of doing that. I thought the async-emacs example was pretty cool: (let ((a (async-emacs '(or (sleep-for 5) 5))) (b (async-emacs '(or (sleep-for 2) 2)))) (+ 1 (await-emacs a) (await-emacs b))) and I can make it not block emacs easily like this: (make-thread (lambda () (print (let ((a (async-emacs '(or (sleep-for 5) 5))) (b (async-emacs '(or (sleep-for 2) 2)))) (+ 1 (await-emacs a) (await-emacs b)))))) Maybe if make-thread was not desirable, the result could be output via asynchronous cat process (something like the trick ielm uses), but that seems unnecessary complication. > e.g. when you need to consult a separate executable/server from within > `jit-lock`, so you need to immediately reply to `jit-lock` saying > "pretend it's already highlighted" spawn some async operation to query > the external tool for info, and run some ELisp when the info comes back > (which may require running some other external tool, after which you > need to run some more ELisp, ...). Sure, if the consumer does not really need the value of the result of the asynchronous computation, just plug in a callback that does something later. In your example, you immediately return a lie and then fix it later asynchronously from a callback. But this breaks down when the consumer already run away with the lie and the callback has no way of fixing it. So this is not what future, async and await are about. Those are about the consumer waiting for truth. It is not about putting stuff to do into callback but more about taking a value out of callback. For putting stuff into callback, the simple macros consume and alet do that. It is trivial with macros. Maybe it is confusing because you describe what the producer does, but not what the consumer does. And in your example, it does not matter what value the consumer receives because the callback will be able to fix it later. In your example, there is no consumer that needs the value of the future. Like in your example, my async* functions return a value (future) immediately. But it is important, that await itself will eventually return a true value which the consumer will use for further computation. >> I think it is quite different. What is the point of futur-deliver, >> futur-fail, futur-pure, futur--bind, futur--join, futur-let*, >> futur-multi-bind when the lisp can figure those automatically? > > When writing the code by hand, for the cases targeted by my library, you > *have* to use process sentinels. `futur.el` just provides a fairly thin > layer on top. Lisp can't just "figure those out" for you. async-process uses process sentinel but this is just an implementation detail specific to asynchronous processes. It does not have to leak out of the future/async/await "abstraction". I am talking about code which takes several futures as input and computes a result. There is no need for future-let* etc because everything "just works" with normal lisp code. Here is a working example again: (seq-reduce ;; compute total length, parallel (faster) #'+ (mapcar (lambda (x) (length (await x))) (mapcar 'acurl '("https://dipat.eu" "https://logand.com" "https://osmq.eu"))) 0) Can futur.el do that? >> Other use-cases would do something different, but once the future is >> computed, it does not change so there is no state to maintain between >> changes of the future. > > I'm not talking about saving some state in the future's value. > I'm talking about saving some state in the "continuations/callbacks" > created by `futur-let*` so that when you're called back you don't need > to first manually re-establish your context. If your example in futur.el actually worked, it would be easier to see what do you mean. How would the examples I provided look like with futur.el? I was not able to figured that out. futur.el is completely broken, e.g. future-new has let instead of let* and throws an error. A future represents a value, not a stream of values. For example, async-process uses a callback but it does not need to re-eastablish any context because the single value the future resolves to "happens" once only. In the case of stream of values, proc-writer is the thing that "re-establishes" the context (because the producer and consumer do different things to the shared buffer at the same time). But that is not about async/await. I think that your confusion is caused by the decision that futur-process-make yields exit code. That is wrong, exit code is logically not the resolved value (promise resolution), it indicates failure (promise rejection). The exit code should just be part of an error, when something goes wrong. Then your example would look like this: (futur-let* ((cmd (build-arg-list)) (out <- (futur-process-make :command cmd :buffer t)) (cmd2 (build-second-arg-list out)) (out2 <- (futur-process-make :command cmd :buffer t))) (futur-pure out2)) or even better: (futur-let* ((out <- (futur-process-make :command (build-arg-list))) (out2 <- (futur-process-make :command (build-second-arg-list out)))) (futur-pure out2)) now it looks almost like my alet example (alet out (build-arg-list) (when out (alet out2 (build-second-arg-list out) (when out2 (print out2))))) which looks more structured and not so flat, and the implementation is much simpler. I think it would be interesting to see a version of await-iter which would queue continuations in an implicit event loop like what javascript does instead of explicit polling loop as I did for simplicity. I have not figured that one out yet.