Open marcoonroad opened 8 years ago
And it means that the current API -- poorly-done under some viewpoint -- must be broken... or maybe I'll create a new module in this library for that.
BUMP:
The implementation is quite trivial, once we use two channels and implement coroutines as synchronized tasks, for instance, the resume
could be defined as:
method resume($value) {
$!input-channel.send($value);
return $!output-channel.receive;
}
And the coroutine function itself would have the following shape:
my $coroutine-function = &yield -> {
...
}
Internally, the &yield
callback to be passed would be:
sub ($value) {
if $*THREAD.id == $!coroutine-thread.id {
$!output-channel.send($value);
return $!input-channel.receive;
}
else {
die "Yield called outside the owner coroutine!";
}
}
BUMP v2:
An ownership system on thread-level may impose a burden. Therefore, it demands an ownership transfer feature as well. On the other side, it is possible as well to implement locks/mutex around the generator/step procedure.
But the mutex only ensures that only one thread enters the generator/step procedure scope, issues and gotchas regarding non-determinism can arise here. It's a trade of tradeoffs to think carefully yet.
To improve performance, coroutines could run on another thread. It would give a non-deterministic scheduling, only if the queue of pending coroutines is randomly shuffled / permuted. By running on another thread, other threads can maintain a bidirectional channel for such coroutine scheduler thread. Streams aren't a good solution 'cause we have only one worker here, it makes no sense to maintain an append-only log structure where the clients and the worker are "coupled" in some sense. To avoiding blocking the coroutine scheduler thread, we can make things more asynchronous, for instance:
resume : ('a, 'b) coroutine -> 'a -> 'b promise
yield : 'b -> 'a promise
produced : ('a, 'b) coroutine -> 'b stream
consumed : ('a, 'b) coroutine -> 'a stream
Where yield
and resume
are non-blocking operations, and produced
and consumed
are stream logs for the execution context of the coroutine. Whenever the coroutine finishes (by returning or throwing), the stream logs are closed and further resume
attempts will fail.
To maintain our desired deterministic coroutine nature, the client just must call await
directly on resume
and yield
results, for example:
...
my $result = await resume($coroutine, $input);
...
:point_right: In this sense, coroutines could be regarded as bidirectional synchronous channels + worker promise, where we feed such worker with inputs and block waiting responses from him.
Implement yield as an expression (instead of just a simple statement), e.g:
where » ≈ → with Lazy Continuation effects.
A new function resume to fill the blocked hole in some coroutines, e.g:
where » ≈ → with Lazy Continuation effects.
An efficient way – and safe – to check the current state of a given coroutine, e.g:
Being more clear, so, here, an yield function will be typed as Str ⊳ Int, giving to the whole coroutine a Coroutine[ supply<Str>, demand<Int> ] typing.
Int x ← yield “example”
With this two-way-passing stuff, we can derive pipes just like/with:
consumer : Coroutine[ demand<α>, supply<β> ]
producer : Coroutine[ demand<γ>, supply<α> ]
Nonetheless, these static/generic/parametric typing rules/guarantees can be discarded in favor of some simple implementation (and as result, unityped and unsafe), thus requiring some additional effects at runtime, e.g, exceptions.