main function setups that.Fun.protect. It also keeps the difference between synchronous and asynchronous function calls small, which in turn allows to sprinkle or retract asynchronous calls in your code more easily.Fun.Async.call without too much surprises.Affect.Cell.Once semantics instead of a rendez-vous or a single shot inclusive action semantics? It wouldn't be natural. Once a function has computed and you still have the call handle value you just want to be able to get that result regardless of whether the function has already returned or not. Trying to be different would clash with point 7. of Choices and guidelines.Do not force functions to get the result of their asychronous function calls.
Alternative would be at the end of the function:
The first idea may make some pattern for imperative parallel or speculative computing (cancel and forget) more involved than they could be – it also means that you consider that asynchronous functions are a ressource which is not a view that we want to take (again point 7. of Choices and guidelines).
The second idea it feels a bit odd to make a difference between values and exceptions. In both cases trying to track if a get was attempted would entail a fair amount of bookeeping (perhaps not in fact).
This is not to say that this decision is not problematic as it may make unexpected exceptions go unoticed. We try to alleviate that by providing Affect.Fun.Async.call_trap_exn for those users who rely on structured concurrency to synchronize, but that remains something the user has to do explicitely and may forget to do.
Why not provide a way to escape the strict structured concurrency discipline, for example by providing a Fun.Async.detached_call, where either you can specify an arbitrary parent or simply makes the call attached to the root asynchronous function. We feel that:
Affect.Action.invoke? If the cancellation is due to something that the action expects, it becomes a "cancelock" that waits forever. So we participate in the choice of an Affect.Action.invoke with an invisible action primitive that raises Cancelled if the function marks as cancelled.Affect.Fun.Async.Cancelled.Affect.Fun.Async.Cancelled. Besides a cancellation mask scope may be invoked regardless of cancellation and we may want to be able to reason about the cancellation mark of the caller in that scope.Why not stick with CML's Event terminology?
We use the term action derived from CCS to get full Milnerization. Thinking in term of actions that you invoke and that return values is easier to get an intuition about than with the notion of event – especially for combinators like Affect.Action.guard. Besides:
Affect.Action.guard.Event (implementing CML events over threads) module upstream.τ event is the type of a synchronous operation that returns a value of type τ when it is synchronized upon" (emphasis is mine). So what it is is not described by the event name.CML | Affect |
|---|---|
Event | |
| |
| |
| N/A we directly |
| |
|
This is not to say that event terminology never makes sense, especially for those actions that involve waiting on something that is not really under our control like waiting for readability – but it becomes weird when it is, like the Port.offer action. For those cases where we wait on something then wait_xxx is a natural way of naming this action of waiting on xxx. Sometimes we can turn that into a proper action for example on asynchronous function, we get the result rather than wait_result, notably because of the set-once cell semantics of call handles.
Why not stick with CML's Channel, send and recv terminology?
send and recv have a remote and asynchronous transmission feel which we dislike.CML | Affect |
|---|---|
Channel | |
| |
|
We don't find mvar and ivar's to be particularly enlightening names if not contradicatory for ivar. We gather these synchronized cells in Affect.Cell module with following names:
CML | Affect |
|---|---|
| |
| |
|
A couple of things to note.
poll, doFn, block), also present in OCaml's implemention, has the advantage of producing a list of enabled events to consider. One of the paper mentions that it enables e.g. to support priorities or fairness. Except if priorities are dynamically set by poll I think the two function design also allows this: we have the list of primitive events before, it's just a matter of random shuffling and/or sorting that list before processing it.Affect.Cell.Drop without a spinlock.Affect.Action.choose, they do so by side effect and if we use the CML API (not the OCaml one, which is rather a callback), it requires to run an asynchronous functions call to handle them which is slightly disasteful and may end up clashing with structured concurrency (more thought is needed :-). As mentioned in the papers it is possible to handle this outside the current core. Basically for now when we iterate over the primitive events to do the poll and block we always pass the same continue and unblock functions. These should be changed for each primitive to add invocations of the nacks for those other primitives that requested it. So it seems that the only disruption would actually happen only internaly to the Action module. It could change the costs of action invocations though.wrap (see section §6.4 of paper). I suspect that our Affect.Action.map, implemented without "horrible continuation hacking" is also less powerful due to the loss of context. We also dislike the fact that the unblocking entities are the ones that end up invoking the mapping function, we'd prefer to have the ready thunk do that back on the scheduler (which would likely also solve the loss of context problem).spawn : (unit -> unit) -> unit. Our enforcement of structured concurrency means that some of the patterns they use (a.k.a. threads everywhere) for example to implement synchronization mecanisms using ports can likely not be used (see for example §5.3.1 in the book). Since that's precisely the style we dislike (data structures having hidden threads) all the better, but we may need new books and have a better assement of what is exactly lost here.Action.guard do_poll and do_block, we throw them into the Action.invoke, should we rather Panic and fail the whole scheduler? See XXX in the code, it could break invariants.Affect.Action.invoke_poll? It's one more effect to handle and it tempts people into performing their own scheduling. Some actions will always return None.only_main_queue. Basically one should not steal only_main ready work on the main queue.choose but to indicate which priority should be given to the waiter and is returned on Affect.Action.Private.Action.Invocation.make.In some sense affect is simply an attempt at icing the cake of Parallel Concurrent ML with structured concurrency and structured cancellation. Synchronous actions are an implementation of the notion first-class synchronous events which can be found in the works mentioned below. They are just a parallel-safe and lock-free implementation of the OCaml Event module.
John Reppy. Synchronous Operations as First-class Values. 1988
The original CML paper.
John Reppy. Concurrent Programming in ML. 1999
Good book on CML with lots of examples, thoughts on concurrency, synchronicity and design rationales.
Kevin Donnelly et al. Transactional events.
Interesting monadic account of CML events, see also this technical report.
John Reppy et al. Parallel Concurrent ML. 2009
Describes the protocol for parallel event synchronization.
Andy Wingo et al. Guile fibers library. 2017 See also this blog post.
Guile implementation of Parallel CML. Our notion of primitive action is rooted in their notion of operation where they reduce the three function of Reppy 1999 to two. We also stole their implementation scheme for ports which avoids spinlocks. Like them we change the terminology, but did not follow theirs.
Here are two personal notes:
unit -> unit thread and channels are use to get out information from these black holes. We hope that in affect, by having structured concurrency with functions unit -> 'a that return values that can be synchronized like a set-once cell, we can put less emphasis on using "threads" and ports. In fact as we noted in the last point here, some of the patterns they use may not be feasible with affectStdlib.Fun.Async where it naturally belongs.Affect.Fun.Async.Call_handler which allows to recover a form of structured effect handling.Fiber.block mecanism was almost the structure of a CML primitive event. Blocking on an asynchronous function call becomes blocking on the action to get its result which is very pleasing. Provide the full power of CML composable's synchronization to the system and unifies the synchronization mecanism for asynchronous function synchronisation, blocking on primitives (system calls) and synchronization structures (ivar, mvar, ports, byoss) which where previously separate and ad-hoc. For example in the previous iteration if you wanted to say, cancel a function after a timeout, you had to spawn a fucntion that would wait the timeout and use the ad-hoc "pick" combinator between two functions. In this new system you can simply ask for the function get a ction and choose it with a timeout primitive action which results in composite action that can be waited on, so it's not optimal.await (previously join) than the previous 2022 iteration of Affect which only had structured cancellation topped with a franken-semi-cooperative cancellation.First design round.