[Pitch] Observation (Revised)

OK so let's imagine a system where an "observer" component wants to observe a value, while another component, the "modifier" wants to modify it.

We have to code the observer.

Let's add some constraints:

  1. The observer displays the value on screen. It's just a way to make the observer concurrency-constrained. Displaying the value on screen requires running on the main thread/actor.
  2. The observer should display the "latest" value. You can understand this informally, as as layperson would say it. For us developers, this means that the observer may miss some updates, but that the observer must always eventually catch up. For a sequence of changes 1, 2, 3, the observer might not display 1 and 2, but it must eventually display 3.
  3. The modifier is the rest of the application. This constraint, or lack thereof, is there in order to explain that we code the observer as independently as possible from the modifier. Goals: local reasoning, decoupling, etc.

This is a lot of words to describe a very common need. But it looks like being very explicit is necessary. My apologies to other readers - I hope you can still recognize some of your own needs in this break down of a thought experiment.

Lemme: because the observer is concurrency-constrained, the "latest value" might be displayed a little bit late (until the change notification reaches the main thread). That's unavoidable, so that's ok. This does not bring any information, but I just want to make sure this has been understood.

So, how do we code the observer?

// First attempt at implementing the observer
func startObserver() { // sync
    // 1. Display the current value
    display(observedObject.value)
    
    // 2. Start an observing task
    Task {
        // 3. Listen to changes
        for await value in ... {
            // 4. Display fresh value
            display(value)
        }
    }
}

Maybe some @MainActor decorations have to be added - but this is the gist.

This first attempt above is not correct, because between the initial display, and the beginning of the observation, some changes may be performed, and they are not notified. We fail the second constraint "The observer should display the latest value".

Even if I relax the third constraint "The modifier is the rest of the application", and make the observer able to tell the modifier "hold on", and "ok I'm ready you can start modifying the value now", I still don't know how to fix the above sample code, because the observer never knows for sure when observation has really started, and the modifier can safely be unleashed. We don't want to unleash the modifier until the observation has started, but the async sequence does not emit anything until a modification has been performed -> we're stuck.

In the end, I don't know how to make a correct implementation of the initial requirements. Those requirements are very common. I actually expect that this is more or less explicitly expected by many developers from this pitch. Some developers might by surprised by the second constraint, which allows the observer to miss some values. Well, this is the consequence of the 1st constraint. We have of give up with synchronous dispatch of changes. It's not something which is easy to give up, I know. That's why I took the time to write the 2nd constraint as clearly as I could - so that everyone can decide if it's an acceptable trade-off for the loss of the synchronous change notification. I think that it is.

If you agree that the described setup is reasonable, maybe you can take this question as a fun challenge to test the pitch against?

I'm not asking for this use case to ship built-in in the pitch - I'm just curious about the mere ability of the pitched apis to support it. Later on, if we establish that it's actually frequent, and actually difficult to implement it correctly (that it's not trivially composable), we might proceed with some support from the standard lib. The first question is just "but is it possible, or not?"

10 Likes