
dear synchronizable event experts, are these functions bad ideas:
(define (unbreakable-evt evt)
(guard-evt (λ () (parameterize-break #f (sync evt)))))
(define (breakable-evt evt)
(guard-evt (λ () (parameterize-break #t (sync evt)))))

@notjack yes, bad idea

(sync (unbreakable-evt evt1) evt2)
-> (sync (parameterize-break #f (sync evt1)) evt2)
!= (parameterize-break #f (sync evt1 evt2))

@ryanc for context I’m trying to use events to represent a sequence of two actions, where the first requires breaks be disabled and the second requires waiting an arbitrary amount of time

also: is that example equivalent to saying (choice-evt (unbreakable-evt e1) e2) != (unbreakable-evt (choice-evt e1 e2))
? because that non-equivalence seems reasonable to me

@notjack The point of events is their cooperative nature. You can sync
on multiple events and the first one that becomes ready is chosen (modulo scheduling granularity, etc). But your [un]breakable-evt
functions construct an event that immediately captures the scheduler’s attention with the inner sync
call, preventing any of its peers from being chosen.

@ryanc ah so the wrappers just completely prevent the event from working together properly with choice-evt
?

how would I go about making events that abstract over sequences of other events? or should I just not do that and use a regular thunk to represent that sort of thing?

if you think of sync
with multiple arguments as implicitly constructing a choice-evt
, then yes

I’m not clear on what you’re trying to do, but it doesn’t sound like the end result is an event.

I’m trying to figure out what the right thing to do about this issue in my disposable
package is: https://github.com/jackfirth/racket-disposable/issues/114

but in general I’m a little unclear on when events are the Right Tool for the Job (TM) as opposed to:
- just blocking the current thread, which is cheap since they’re not OS threads
- spawning a new thread to do something
- using engines, maybe?
- using promises / streams / other stuff that seems to be mostly for lazy compute instead of lazy I/O

is it a good idea to think of events as only the right thing to use if there’s some meaningful way of applying choice-evt
to choose one event and nack others? if I want to wait on a bunch of completely independent events should I just spawn one thread per event?

re that issue: Are you disabling breaks just to prevent interruptions between allocation and registration? If so, I think the right answer in cases like this is to disable breaks but then use tcp-accept/enable-break
so that users can break out of waiting for the connection.

yes that’s exactly why breaks are disabled - specifically by functions like call/disposable
and acquire-global
that actually use them to do stuff

that makes sense but I can’t shake the feeling I’m doing something wrong with disposables and events

Well, disabling breaks doesn’t stop kill-thread
or the thread being killed when the custodian that owns it is shut down.

right, which I’m fine with - disposables are for network-y things where there’s no hard guarantees anyway

I think the thing I’m stuck on is: how do I call and wait on two functions concurrently, getting both their results, while ensuring breaks are propagated to both functions? the disposable-apply
function is the main thing that makes me suspect I should do something clever with events instead of what it does now, which is use delay/thread
to acquire each disposable in a child thread

pretty sure that’s broken as-is for break propagation

The event system cannot handle things like “if both of these events are ready, then choose both and proceed”. So don’t worry if you haven’t found a clean way of doing that; it’s hard.

I’m less worried about that part and more worried about avoiding accidentally serializing independent operations

I want disposables to make it easy to open two unrelated connections to totally different servers without starting the second connection waiting on the first connection finishing, successfully or otherwise

but I want breaks to propogate to both “open connection” operations since either could take an arbitrary amount of time

(I was going to point you at the code for call-in-nested-thread
, but that seems to be implemented in C.)

I was looking at the docs for it but I’m unsure how that would work with calling two nested threads

It doesn’t; I was going to suggest adapting the code to multiple threads.

sending patches to racket’s c codebase is probably not something I’m looking to do

both for tech and social reasons

;; parallel-apply : (Listof (-> Any)) -> (Listof Any)
;; Applies the given list of thunks in parallel, propagating
;; breaks to the evaluation threads.
(define (parallel-apply thunks)
(define result-boxes (map (lambda _ (box #f)) thunks))
(define threads null)
(with-handlers ([exn:break?
(lambda (e)
(for-each break-thread threads)
(raise e))])
(start-atomic)
(set! threads
(for/list ([thunk (in-list thunks)]
[result-box (in-list result-boxes)])
(thread (lambda () (set-box! result-box (thunk))))))
(end-atomic)
(for-each thread-wait threads))
(map unbox result-boxes))

You could also create a custodian for the nested threads and shut it down, but that would be escalating a break to the equivalent of kill-thread
…

does that handle if one of the thunks raises an exception?

no, but that’s not hard to add

also: it’s so odd to me that nobody in 20 years of racket history has built a function with type Evt a -> Evt b -> Evt (a, b)
, sync choice caveats aside

replace-evt
gets so close to it

@notjack can’t you use handle-evt
to do that by syncing on the other one in the function?

@samth I can, but then the events get sequenced unnecessarily and I lose concurrency

I was thinking (handle-evt (choice-evt a b) ...)

oh that’s what you mean, totally misread

it might be a little tricky to figure out which one was chosen

consider (both-evt some-channel never-evt)

maybe fmap-ing each with a gensym first?

@ryanc that’s never going to work with the signature @notjack wants

so the choice will pick the channel, and then the wrap will wait forever

@samth exactly; that’s why that function doesn’t exist

but replace-evt
exists

you get the same problem with (replace-evt some-channel (const never-evt))

I think

no, because that’s an event, not a computation

so you can sync
on it and something else

instead of just waiting forever

@samth I’m confused - which things are you referring to as an event and as a computation

what I suggested was more like (replace-evt some-channel (lambda _ (sync never-evt)))
which is bad

ah, I see what you mean

so would using replace-evt
instead of wrap/handle be a not/less bad way?

I think so

and you want to wrap the second evt to produce the pair

I’m okay with the same caveat that replace-evt
has, where not choosing the returned pair event does not imply the left and right events weren’t chosen

Let me summarize: there are some transactions that cannot be expressed as events. In particular, you can’t express things like “synchronize only if both events are ready”. You can synchronize on at a time, but then you risk getting stuck halfway.

so not all transactions can be events - but should all events be transactions? does it make sense to use events for things that aren’t “transaction-y”? I’m only reaching for them because I don’t know what else to use for doing two independent things at once, aside from the manual thread spawning and exception+break shuffling

it seems like replace-evt
implies that events have more use beyond transactions and as long as the user is aware of that, that’s not necessarily a bad thing

Most events consist of a little transaction followed by some arbitrary Racket wrapper/handler code. But the transactional part has to be one of the things the Racket schedule can handle transactionally.

I’m not really sure what replace-evt
is useful for.

it was added around 6.1 by @mflatt in this commit: https://github.com/racket/racket/commit/bc69a9b05cf7d8fb21353d9473c73d28c323e1fa

I think this is the thread that led to it: https://www.mail-archive.com/dev@racket-lang.org/msg11562.html

it’s weird to have replace-evt
, handle-evt
, and not have some sort of both-evt
function, because replace-evt
makes events a monad and handle-evt
makes them a functor

-ish

a both-evt
function would be the middle-ground that makes events an applicative functor

cutely, I think haskell parsing libraries wanted applicatives for the same reasons I do: they let you express that things are independent and get more free concurrency/parallelism

To clarify: I’m not sure replace-evt
actually/generally solves the class of problems that Jan and/or Matthew hoped it would solve.

does a sequence of transactions where individual transactions are handled transactionally by the scheduler but not the whole sequence make sense as an event?

that seems like the main use case of replace-evt
to me


disregarding multiple values, I think this works for what I want:
(define (pair-evt a b)
(define left (handle-evt a (cons 'left _)))
(define right (handle-evt b (cons 'right _)))
(replace-evt (choice-evt left right)
(match-lambda
[(cons 'left v) (handle-evt b (cons v _))]
[(cons 'right v) (handle-evt b (cons _ v))])))

specifically, (sync (choice-evt (pair-evt A B) C))
may result in either A+B
, C
, A+C
, or B+C
being chosen for synchronization - but not all three and no event is chosen more than once

@notjack make a package!

@samth not until I make sure @mflatt can do a code review :p

make a package and then ask him :slightly_smiling_face:

eventually!

@notjack Looks right, except that I think you meant a
instead of b
in the last line of pair-evt

oops

Yay, events!

I use this a lot: (define (all-evts . es)
(if (null? es)
(handle-evt always-evt (λ _ #t))
(replace-evt (apply choice-evt (map (λ (e) (handle-evt e (λ _ e))) es))
(λ (e) (apply all-evts (remq e es))))))

and this (define (seq-evt* maker0 makers)
(foldl (λ (maker evt) (replace-evt evt maker)) (maker0) makers))
(define (seq-evt maker0 . makers)
(seq-evt* maker0 makers))

last one (define (loop-evt* maker0 makers)
(define loop (λ _ (seq-evt* maker0 (append makers (list loop)))))
(loop))
(define (loop-evt maker0 . makers)
(loop-evt* maker0 makers))

Then I can make consistent chains of events and use sync
as an event loop driver.

An example of the former: (sync
(seq-evt (λ () (port-closed-evt in-port)) die)
(seq-evt (λ () (recv-evt π)) (λ (msg) (if (eof? msg) quit (emit-evt msg)))))

(sync
(loop-evt (λ () (recv-evt π1)) (λ (msg) (give-evt π2 msg)))
(loop-evt (λ () (recv-evt π2)) (λ (msg) (give-evt π1 msg)))
π1 π2)
;; where π1,π2 are processes exchanging messages

I have no idea if this is wise, or even sound.

An embedded DSL could make this fun to use. (concurrent
(seq (wait π) (emit eof) (die))
(seq (let ([msg (recv)]) (if (eof? msg) (quit) (emit msg)))))
(concurrent
(choice
(loop (give π2 (recv π1)))
(loop (give π1 (recv π2)))
π1 π2))

@dedbox those helpers and some others are things that should probably go in a package together somewhere

@notjack others?

@dedbox off the top of my head, here are some things that might be useful in making event chains more readable:
pair-evt :: (Evt a, Evt b) -> Evt (a, b)
list-evt :: [Evt a] -> Evt [a]
apply-evt :: (Evt (a -> b), Evt a) -> Evt b
seq-evt :: (Evt a, Evt b) -> Evt b
call-evt :: Thunk a -> Evt a
call-evt* :: Thunk a -> Thunk (Evt a)
wait-evt :: Evt a -> Evt ()
deadline-evt :: (Evt a, Duration) -> Evt (Maybe a)
fold-evt :: (Evt a, a -> Either (Evt a) b) -> Evt b
forever-evt :: Evt a -> Evt Void

(but with variadic signatures instead of pair-based signatures)

oh, cool!

Sometimes, cooperative concurrency is easier to think about.

oh and one more: const-evt :: a -> Evt a

the call-evt
one is probably the trickiest - that would encapsulate spawning a nested thread to call a function and making sure exceptions and breaks are shuffled between the two correctly