all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
From: Tomas Hlavaty <tom@logand.com>
To: Stefan Monnier <monnier@iro.umontreal.ca>
Cc: Jim Porter <jporterbugs@gmail.com>,
	Karthik Chikmagalur <karthikchikmagalur@gmail.com>,
	Thomas Koch <thomas@koch.ro>,
	"emacs-devel@gnu.org" <emacs-devel@gnu.org>
Subject: Re: continuation passing in Emacs vs. JUST-THIS-ONE
Date: Mon, 10 Apr 2023 23:47:16 +0200	[thread overview]
Message-ID: <87leizif4r.fsf@logand.com> (raw)
In-Reply-To: <jwvpm8lspqm.fsf-monnier+emacs@gnu.org>

On Sun 02 Apr 2023 at 22:09, Stefan Monnier <monnier@iro.umontreal.ca> wrote:
>> (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 languages.
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 = 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 — it's always better to run promises concurrently 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 => {
    setTimeout(() => {resolve(sec);}, sec * 1000);
  });
}
async function test() {
  const a = sleep(9);
  const b = sleep(8);
  const z = 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.



  parent reply	other threads:[~2023-04-10 21:47 UTC|newest]

Thread overview: 53+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-03-11 12:53 continuation passing in Emacs vs. JUST-THIS-ONE Thomas Koch
2023-03-12  1:45 ` Jim Porter
2023-03-12  6:33   ` tomas
2023-03-14  6:39   ` Karthik Chikmagalur
2023-03-14 18:58     ` Jim Porter
2023-03-15 17:48       ` Stefan Monnier
2023-03-17  0:17         ` Tomas Hlavaty
2023-03-17  3:08           ` Stefan Monnier
2023-03-17  5:37             ` Jim Porter
2023-03-25 18:42             ` Tomas Hlavaty
2023-03-26 19:35               ` Tomas Hlavaty
2023-03-28  7:23                 ` Tomas Hlavaty
2023-03-29 19:00                 ` Stefan Monnier
2023-04-03  0:39                   ` Tomas Hlavaty
2023-04-03  1:44                     ` Emanuel Berg
2023-04-03  2:09                     ` Stefan Monnier
2023-04-03  4:03                       ` Po Lu
2023-04-03  4:51                         ` Jim Porter
2023-04-10 21:47                       ` Tomas Hlavaty [this message]
2023-04-11  2:53                         ` Stefan Monnier
2023-04-11 19:59                           ` Tomas Hlavaty
2023-04-11 20:22                             ` Stefan Monnier
2023-04-11 23:07                               ` Tomas Hlavaty
2023-04-12  6:13                                 ` Eli Zaretskii
2023-04-17 20:51                                   ` Tomas Hlavaty
2023-04-18  2:25                                     ` Eli Zaretskii
2023-04-18  5:01                                       ` Tomas Hlavaty
2023-04-18 10:35                                       ` Konstantin Kharlamov
2023-04-18 15:31                                         ` [External] : " Drew Adams
2023-03-29 18:47               ` Stefan Monnier
2023-04-17  3:46                 ` Lynn Winebarger
2023-04-17 19:50                   ` Stefan Monnier
2023-04-18  2:56                     ` Lynn Winebarger
2023-04-18  3:48                       ` Stefan Monnier
2023-04-22  2:48                         ` Lynn Winebarger
2023-04-18  6:19                     ` Jim Porter
2023-04-18  9:52                       ` Po Lu
2023-04-18 12:38                         ` Lynn Winebarger
2023-04-18 13:14                         ` Stefan Monnier
2023-04-19  0:28                           ` Basil L. Contovounesios
2023-04-19  2:59                             ` Stefan Monnier
2023-04-19 13:25                               ` [External] : " Drew Adams
2023-04-19 13:34                                 ` Robert Pluim
2023-04-19 14:19                                   ` Stefan Monnier
2023-04-21  1:33                                     ` Richard Stallman
2023-04-19  1:11                           ` Po Lu
2023-04-17 21:00                   ` Tomas Hlavaty
2023-03-14  3:58 ` Richard Stallman
2023-03-14  6:28   ` Jim Porter
2023-03-16 21:35 ` miha
2023-03-16 22:14   ` Jim Porter
2023-03-25 21:05 ` Tomas Hlavaty
2023-03-26 23:50 ` Tomas Hlavaty

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=87leizif4r.fsf@logand.com \
    --to=tom@logand.com \
    --cc=emacs-devel@gnu.org \
    --cc=jporterbugs@gmail.com \
    --cc=karthikchikmagalur@gmail.com \
    --cc=monnier@iro.umontreal.ca \
    --cc=thomas@koch.ro \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.