notjack
2017-12-11 11:55:28

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

ryanc
2017-12-11 12:02:51

@notjack yes, bad idea


ryanc
2017-12-11 12:03:31

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


notjack
2017-12-11 12:05:16

@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


notjack
2017-12-11 12:06:44

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


ryanc
2017-12-11 12:14:05

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


notjack
2017-12-11 12:16:58

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


notjack
2017-12-11 12:17:26

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?


ryanc
2017-12-11 12:17:28

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


ryanc
2017-12-11 12:20:13

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


notjack
2017-12-11 12:20:43

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


notjack
2017-12-11 12:23:39

but in general I’m a little unclear on when events are the Right Tool for the Job (TM) as opposed to:

  1. just blocking the current thread, which is cheap since they’re not OS threads
  2. spawning a new thread to do something
  3. using engines, maybe?
  4. using promises / streams / other stuff that seems to be mostly for lazy compute instead of lazy I/O

notjack
2017-12-11 12:26:05

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?


ryanc
2017-12-11 12:32:33

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.


notjack
2017-12-11 12:33:31

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


notjack
2017-12-11 12:34:19

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


ryanc
2017-12-11 12:40:33

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


notjack
2017-12-11 12:41:22

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


notjack
2017-12-11 12:45:01

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


notjack
2017-12-11 12:45:30

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


ryanc
2017-12-11 12:51:25

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.


notjack
2017-12-11 12:53:01

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


notjack
2017-12-11 12:54:31

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


notjack
2017-12-11 12:55:34

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


ryanc
2017-12-11 12:59:41

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


notjack
2017-12-11 13:00:09

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


ryanc
2017-12-11 13:05:58

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


notjack
2017-12-11 13:07:23

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


notjack
2017-12-11 13:07:33

both for tech and social reasons


ryanc
2017-12-11 13:16:24

;; 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))


ryanc
2017-12-11 13:19:00

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


notjack
2017-12-11 13:23:50

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


ryanc
2017-12-11 13:24:34

no, but that’s not hard to add


notjack
2017-12-11 13:25:18

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


notjack
2017-12-11 13:26:30

replace-evt gets so close to it


samth
2017-12-11 13:30:40

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


notjack
2017-12-11 13:31:04

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


samth
2017-12-11 13:31:28

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


notjack
2017-12-11 13:32:46

oh that’s what you mean, totally misread


samth
2017-12-11 13:33:17

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


ryanc
2017-12-11 13:33:17

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


notjack
2017-12-11 13:33:32

maybe fmap-ing each with a gensym first?


samth
2017-12-11 13:33:45

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


samth
2017-12-11 13:34:11

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


ryanc
2017-12-11 13:34:39

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


notjack
2017-12-11 13:35:05

but replace-evt exists


notjack
2017-12-11 13:35:25

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


notjack
2017-12-11 13:35:41

I think


samth
2017-12-11 13:35:43

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


samth
2017-12-11 13:35:53

so you can sync on it and something else


samth
2017-12-11 13:35:59

instead of just waiting forever


notjack
2017-12-11 13:36:49

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


samth
2017-12-11 13:37:29

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


notjack
2017-12-11 13:38:16

ah, I see what you mean


notjack
2017-12-11 13:38:22

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


samth
2017-12-11 13:38:37

I think so


samth
2017-12-11 13:39:00

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


notjack
2017-12-11 13:39:06

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


ryanc
2017-12-11 13:40:10

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.


notjack
2017-12-11 13:42:14

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


notjack
2017-12-11 13:43:26

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


ryanc
2017-12-11 13:45:31

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.


ryanc
2017-12-11 13:47:17

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


notjack
2017-12-11 13:48:18

samth
2017-12-11 13:49:05

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


notjack
2017-12-11 13:50:56

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


notjack
2017-12-11 13:51:11

-ish


notjack
2017-12-11 13:52:13

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


notjack
2017-12-11 13:53:24

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


ryanc
2017-12-11 13:55:08

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


notjack
2017-12-11 13:56:37

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


notjack
2017-12-11 13:56:51

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



notjack
2017-12-11 14:12:51

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))])))

notjack
2017-12-11 14:14:25

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


samth
2017-12-11 14:18:29

@notjack make a package!


notjack
2017-12-11 14:19:10

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


samth
2017-12-11 14:19:24

make a package and then ask him :slightly_smiling_face:


notjack
2017-12-11 14:23:46

eventually!


mflatt
2017-12-11 14:28:45

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


notjack
2017-12-11 14:28:59

oops


dedbox
2017-12-11 21:33:15

Yay, events!


dedbox
2017-12-11 21:33:35

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


dedbox
2017-12-11 21:35:47

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


dedbox
2017-12-11 21:36:22

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


dedbox
2017-12-11 21:37:10

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


dedbox
2017-12-11 21:42:25

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


dedbox
2017-12-11 21:44:40

(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


dedbox
2017-12-11 21:45:29

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


dedbox
2017-12-11 22:15:25

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


notjack
2017-12-11 22:55:21

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


dedbox
2017-12-11 23:19:09

@notjack others?


notjack
2017-12-11 23:32:50

@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

notjack
2017-12-11 23:33:10

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


dedbox
2017-12-11 23:37:45

oh, cool!


dedbox
2017-12-11 23:39:02

Sometimes, cooperative concurrency is easier to think about.


notjack
2017-12-11 23:39:48

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


notjack
2017-12-11 23:41:39

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