fibers icon indicating copy to clipboard operation
fibers copied to clipboard

Something like promises

Open pukkamustard opened this issue 8 months ago • 5 comments

It might be useful to add something like a condition that can hold arbitrary values. I'll call this a promise for the moment and it might expose something like this:

- `make-promise`
- `promise?`
- `resolved?`
- `resolve`
- `promise-wait` (calling this promise-wait to distinguish from the condition wait)

An example where this is useful would be to signal completion of a request with return value.

In some code bases (e.g. shepherd) a reply channel is used for something similar. A request is posted to a pool of worker along with a newly created channel that is used to send the return value of the request back to the requestee. Here an example from (shepherd services):

(define (service-name-count)
  "Return the number of currently-registered service names."
  (let ((reply (make-channel)))
    (put-message (current-registry-channel)
                 `(service-name-count ,reply))
    (get-message* reply 5 'no-reply)))

(get-message* is like get-message but also does timeout handling).

Using promises this might be (omitting the timeout logic):

(define (service-name-count)
  "Return the number of currently-registered service names."
  (let ((promise (make-promise)))
    (put-message (current-registry-channel)
                 `(service-name-count ,promise))
    (wait-promise promise)))

Code looks very similar, but there seem to be some differences:

  • Multicast: A promise can have multiple waiters, whereas the reply message in the channel is only received by a single waiter.
  • A promise can be re-used for caching the response to a request.

From what I understand this can be implemented using make-base-operation. Still, this might be useful enough to include in fibers itself?

Naming

The name promise is used in the manual as an example for monadic style concurrency. The promise proposed here does not force the usage of monads.

Guile has promises for delayed execution (https://www.gnu.org/software/guile/manual/html_node/Delayed-Evaluation.html).

Maybe calling it something else would make sense.

pukkamustard avatar Dec 10 '23 07:12 pukkamustard

cc: @civodul for insight from somebody who has been using reply channels.

pukkamustard avatar Dec 10 '23 07:12 pukkamustard

Howdy @pukkamustard!

In the cases where I use a "reply channel", it's typically a remote procedure call (RPC) pattern so I don't need multicast nor caching.

There are cases in the Shepherd with some sort of multicast: for instance, if multiple fibers call (start-service s), then one of them takes "ownership" and the rest of them block on a condition variable. It's not entirely clear to me that the proposed abstraction would help though, because it's very much about the domain logic of service startup.

Regarding the name, I'd avoid "promise" because it's already taken (comes from R5RS or perhaps earlier than that), including make-promise. Naming is hard. :-)

Probably worth checking what the Spritely folks think, too!

Ludo'.

civodul avatar Dec 14 '23 18:12 civodul

It might be useful to add something like a condition that can hold arbitrary >values. I'll call this a promise for the moment and it might expose something like this:

Basically, that's a condition variable + atomic box holding the result, where the result may only be set once (and permission to read the premise is separated from permission to set the result, but that's easy to accomplish by letting the constructor return two objects.)

(the atomicness might not even be necessary because of the condition variable, but I don't know if signal-condition!/wait-operation actually does the proper C acquire/release stuff ...)

Low-level interface:

(make-promise) -> promise object + setter

(for RPC stuff, you can send (query . setter) to the remote channel inside a spawn-fiber for asyncness).

higher-level:

(force ...): wait-operation + read the box

(eager ...): pre-signal the new promise

(lazy ...) (delay ...): I don't think the single condition is sufficient for this. (I think it should be possible to implement them somehow, but I'm insufficiently familiar with the SRFI implementation to be sure and actually say how.)

(async ...): make promise and spawn a fiber evaluating the expression and setting the result.

Higher-level, somewhat more limited: delay/force/lazy/eager(?).

From what I understand this can be implemented using make-base-operation. Still, this might be useful enough to include in fibers itself?

Just use wait-operation on the condition variable, and use wrap-operation to read the result. Also, about time-outs: you get those for free by using 'choice-operation'.

About ‘multicast’: you get that for free because 'wait-operation' is ‘multicast’.

Regarding the name, I'd avoid "promise" because it's already taken (comes from R5RS or perhaps earlier than that), including make-promise. Naming is hard. :-)

If the 'delay/lazy' part is potentially implementable, I would name it "promise" (or "premise", I need to look up proper spelling), because it is the same concept as the R5RS or SRFI stuff. #:prefix is a thing; module imports can easily be renamed. Or just add name it 'concurrent-promise' to avoid collisions.

Also, perhaps at some point in the future, Fibers is made part of Guile itself, the SRFI-45 promised and Fiber promises can be unified!

My naming proposal: waitable-async-value -- you can wait on it, the waiting/setting doesn't happen in sync, and it is a single value (not a box that can be set multiple times!).

emixa-d avatar Dec 15 '23 21:12 emixa-d

(for RPC stuff, you can send (query . setter) to the remote channel inside a spawn-fiber for asyncness).

Also, you need to wrap 'setter' in a weak reference to make sure that the promise contains a strong reference to setter) and some guardian stuff to signal a condition to tell that fiber to stop if nobody is interested in the result promise anymore, to make sure that the fiber stops eternally asking a potentially-dead remote fiber, wasting resources.

(That's for pure querying stuff, should be optional as that's potentially undesired for more stateful stuff.)

emixa-d avatar Dec 15 '23 21:12 emixa-d

Regarding the name, I'd avoid "promise" because it's already taken (comes from R5RS or perhaps earlier than that), including make-promise. Naming is hard. :-)

In Concurrent ML these are called iVars (immutable variables): they and another flavor of "synchronous variables", mVars, originated in the Id programming language. Reppy's Concurrent Programming in ML introduces them in section 2.6.2 and presents them with more detail in section 5.3. In particular, it notes that, in RPC scenareos, an efficient implementation of iVars allows writeVar to be non-blocking and "can save around 35% of the synchronization and communication costs" compared to an implementation using channels internally.

There is an Apache-2.0 implementation in a Racket package: https://docs.racket-lang.org/syncvar/

LiberalArtist avatar Dec 16 '23 03:12 LiberalArtist