Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Something like promises #100

Open
pukkamustard opened this issue Dec 10, 2023 · 5 comments
Open

Something like promises #100

pukkamustard opened this issue Dec 10, 2023 · 5 comments

Comments

@pukkamustard
Copy link

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
Copy link
Author

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

@civodul
Copy link
Collaborator

civodul commented Dec 14, 2023

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'.

@emixa-d
Copy link
Collaborator

emixa-d commented Dec 15, 2023

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
Copy link
Collaborator

emixa-d commented Dec 15, 2023

(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.)

@LiberalArtist
Copy link

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants