i think the proposal provides some interesting ideas that are probably useful in certain contexts, but the guarantees the type aims to provide around value delivery still seem uncomfortably vague. additionally, the implementation still has some undesirable behaviors and it's unclear if/how they will be handled.
the following are, in no particular order, some further thoughts on the design and current implementation. some are similar to those raised in the pitch thread and implementation PR, but i think are worth repeating here:
Multi-consumption is prone to race conditions
the proposal states that if there are multiple consuming Tasks, then:
... tasks will get the same values upon the same events.
with the current implementation, this seems like behavior that cannot in general be upheld. suppose we have an Observed
closure that produces increasing integer values v_i
, and two iterators, I_1
and I_2
that both consume the sequence's values. assume the source closure invocations and iteration all occur on the same actor (let's say @MainActor
). the following sequence of events can occur:
I_1
& I_2
both suspend awaiting a 'will set' event
- the
Observed
input value increments to v_1
, scheduling resumption of the iterators
- a second increment of the input to
v_2
is asynchronously scheduled on the source isolation
I_1
resumes and reads v_1
- the
Observed
input value increments to v_2
I_2
resumes and reads v_2
so in this case, the two iterators will see different values, despite the fact that all events take place on the same isolation, and they were initially suspended awaiting the same 'will set' trigger.
Multi-consumer initial value delivery
currently only one iterator will get the initial value of the sequence. this was raised a couple times in the pitch thread, but the proposal doesn't clarify what the expected behavior is.
Dependency changes while processing values breaks iteration
in the current implementation, if an iterator's consumer is processing a value returned by next()
and the withObservationTracking
change handler fires before the consumer installs and awaits its next 'will set' continuation, then, due to the 'one-shot' nature of the change handler, the sequence effectively breaks and the iterator remains suspended indefinitely. subsequent changes to the Observed
object go unseen because the 'chain' of observations is broken in this case. the proposal obliquely touches on this in the 'behavioral notes' section with the 'producer outpaces consumer' example. it suggests that all values should eventually be seen, but this isn't how the implementation works today, and further clarity on how this is intended to be handled would be good.
IMO this is probably the most serious issue with the current implementation, since it's fairly easy to cause (even inadvertently), and can result in an iterator being entirely non-functional.
Use of @isolated(any)
may be exploiting a compiler bug
sort of a tangential and implementation specific observation, but the original design was changed to have the Observed
closure be marked @isolated(any)
. this makes sense as it is supposed to capture a fixed isolation upon initialization. however, it is a synchronous closure, and as such should not be callable within the context of a withObservationTracking
block, as @isolated(any)
functions must generally be awaited. the implementation gets around this today by seemingly relying on a compiler bug that allows an isolation dropping function conversion to take place when calling the function through Result(catching:)
.