SE-0395: Observability

Cycling back to a few points of feedback:

There is a slightly inaccurate but founded concern about flickering UI. I think it might be better to phrase that as more so the issue underlying that; the concern is consistency. Which is a very reasonable worry. The point of consistency problems crops up (paraphrasing a few posts by folks) that if a property will be observed and the change in that property only happens once (and never happens again) and the confluence of events is such that the property observation via values(for:) is called asynchronously (e.g. in a Task) the first value could get lost.

After working through all of the ramifications and implications of this; there are a few take-aways. 1) that concern is not only reasonable, but also is a true problem. 2) prepending the first value does have some consequence, but in general is probably fine.

Let me iterate what I have discerned:
The first point of iteration should be ideally where any work is done, however... getting that first value MUST occur in the same isolation domain as the access to the object. Furthermore the first value may not be desirable in all circumstances. Lastly, holding that value might incur some sort of lifecycle impact.

Options:

  1. Hold the Observable type and defer access to the first value until the first point of iteration.
    This option does not address the missing first value problem but also poses a retain cycle issue.
  2. Grab the initial value immediately at construction.
    This option does address the "first value" problem but comes at a cost of extra overhead of storage. This also has an interesting alteration to the protocols; it forces the values(for:) to be isolated; because what happens if the Observable type is @MainActor bound? The keypath based access MUST follow the same rules as the property in question.

In my view option 1 might have an attractive characteristic that it defers access, but since it does not solve the issue it is disqualified.

Option 2 does validly solve the issue. It could be a option to the values(for:) but to be honest if folks don't want that first value... they could just say values(for:...).dropFirst(). But it means that the method for getting values must share the same isolation as the type. Which in the grand scheme of things is probably a reasonable alteration (having it be nonisolated was only really a bonus and not really a core part of the API).

That was all a very long winded way of stating; I agree that providing the first value can be accomplished, and should be part of the behavior for values(for:).

This now brings us to changes(for:). To me it would be strange that values(for:) had an "initial value" but changes(for:) did not. So for the same reasoning that should also have an "initial value". The initial would be claiming that the change went from "observing nothing" to "observing all the items". Meaning that it would emit a change containing all tracked properties of that observation.

I have some additional responses/musing/mulling-over of the synchronous versus asynchronous part of things but I will follow up later with that when my thoughts are a bit more congealed with that.

The behavioral change outlined here will be included with an update to the proposal. As well as an update to the implementation.

Thanks for all of the discussion so far - this is definitely the type of feedback that makes things much more robust and I appreciate your patience with me on navigating all of the feedback.

26 Likes