Brief summary of idea\proposal:
Allow closures and functions to capture control flow environment, with support for following effects: fallibility
,asynchrony
& multiplicity
.
-
fallibility
is achieved through introduction of a return-to-label (diverging) expression:return 'label val
. It is terminating effect; -
asynchrony
is done by allowing closure to capture async context's implicit values and use them to await a future in a closure:|| await(some_captured_fut)
. Resulting closure's state is stored in and polled by parent future; -
multiplicity
is provided via capturing generator's environment andyield
'ing from (another) generator context.
Pain points:
- light weight syntax for providing labels to their consumers;
- inconsistent syntax of
asynchrony
; - giving a labels for closure and generators contexts.
Solutions:
-
Second class labels.
We introduce labels as second class citizens. In input positions, by functionality:-
fn gimme_label(lab: label(u64))
- is for terminating return effect; -
fn gimme_gen(parent_scope: label(u64 -> bool))
- is for consuming labels of generators' scopes.
Because of labels being second class citizens we impose restrictions:
- No renaming other than at function boundary;
- As a consequence, there is no bindings of type "label".
Example:
fn takes_a_label(a: label(u64 -> bool)) -> ! { let mut acc = 0; loop { acc += yield 'a (acc % 2 == 0) }; }
And btw, here I had to use yield as an expression approach - I have no access to what is actually being mutated.
-
-
Possible use of async closure's syntax.
Instead of trying to justify inconsistent use of await outside of async function/block(/closure),
I opt to reuse async closure syntax for creating such TPC closures:
Example:async mapper(iter: impl Iterator<Item=Struct>) -> Vec<Struct2>{ iter .map(async |it|{ process(it).await }) //note, this creates iterator tied to local scope .collect(); ... }
-
A syntax is like
'label: |arg| {...}
.
Example (using previously definedtakes_a_label
):... // has type: `impl Generator<u64,Yield = bool,Return = !>` let gen_cl = 'lab: |num: u64| { takes_a_label('lab) // diverges }; ...
HOF.
Section which they deserve.
Such cryptic syntax is not for no reason:
The primal goal was to allow HOF to write signatures like this:
-
async fn hof(f: async fn(label(u64)) -> Struct )
; - or this:
/// it will "fold" a coroutine
fn folder<T>(
init: T,
f: fn(label(Struct -> Struct2)),
fld: fn(T,Struct2)->(T,Option<Struct>),
)
Drawbacks:
-
label
is a keyword now.
Unresolved questions:
- exact rules for async closure syntax: when it creates a TPC async closure?; when a plain async closure?;
- real cost of second class labels.
Things to note:
- Given all of these one might think that this is a coroutines proposal, but it's not really one because of imposed restrictions on how labels flow between functions.
- To avoid unnecessary monomorphic copies of label taking code, we can give them an actual runtime representation. Then effectful code will receive pointers to where to store next yield value and whom to callback on next yield.