[Accepted] SF-0015: `AttributedString` Tracking Indices

Hello Swift community,

The review of SF-0015: AttributedString Tracking Indices begins now and runs through Jan 22, 2025.

Reviews are an important part of the Swift-Foundation evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by email or DM. When contacting the review manager directly, please include proposal name in the subject line.

Trying it out

If you'd like to try this proposal out, you can check out the pull request.

What goes into a review?

The goal of the review process is to improve the proposal under review
through constructive criticism and, eventually, determine the direction of
Swift-Foundation. When writing your review, here are some questions you might want to
answer in your review:

  • What is your evaluation of the proposal?
  • Does this proposal fit well with the feel and direction of Swift-Foundation?
  • If you have used other languages or libraries with a similar
    feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick
    reading, or an in-depth study?

More information about Swift-Foundation review process is available at

https://github.com/apple/swift-foundation/blob/main/CONTRIBUTING.md

Thank you,

Tina L
Review Manager

6 Likes

Seems useful, although my personal familiarity with this API is low.

Minor suggestion: The label updating is (at least to me) suggestive of in-place updating, which this obviously does not do; it seems that since these are described as "tracking" APIs, tracking might be an obvious, searchable, and fitting alternative label.

Major question: If a user would prefer to keep the current guarantees of totally-valid-else-crash, the proposed fallback semantics of using the UTF-8 offset and potentially giving an unexpected result without warning could actually be undesirable. Of course, you are providing a new API to check validity, but it makes recovering the current behavior significantly more
onerous at the call site. Would it be worthwhile for, say, at least the most essential subscripting APIs to provide a labeled "stringent" equivalent that users could migrate to if they desire to keep the present-day behavior?

I think this is a useful addition, so +1 from me.

I can imagine that quite a few usage scenarios around tracking indices into strings under mutation will come up, so the API should be open for extension, which I think it is. The suggested addition seems like a good first step.

As for the argument label name, I agree that tracking might be a better choice than updating:

let updatedRangeOfJumped = attrStr.transform(tracking: rangeOfJumped) {

I also like the future directions idea ofTrackedAttributedString and the ability to "keep a stored AttributedString and set of indices in-sync". This seems very useful for e.g. highlighting search results in a piece of text that may still be edited (while showing highlights).

1 Like

I chose to go with updating: since typically Swift APIs use the -ing suffix to indicate that a new value is returned rather than an in-place update (for example, AttributedString.setAttributes(_:) vs. AttributedString.settingAttributes(_:)). So in my opinion the updating actually denotes returning an updated range rather than an in-place mutation of the range via an inout variable (and that's why the function is transform instead of transforming - since it performs an in-place mutation of the AttributedString). I chose to avoid tracking because it wasn't clear to me what "tracking" meant specifically whereas I had felt that "updating indices" was semantically clearer. There's also precedent for the term "update" in a handful of stdlib APIs but I didn't see any precedent for the word "tracking". Do you think there are other examples of APIs where tracking might fit well instead of "sticking out" as a new word?

That's a fair question, and I'll think on this some more. I'm not sure I'd say it'd be terrible worthwhile to just have a stringent subscripting equivalent because I think the "essentials subscripting APIs" category could easily balloon into a very large surface area to accomplish what any developer might want. In general the design here was to make this the best behavior for most AttributedString uses (which, for mutations, is largely editable text) - do you have a concrete example where a developer might strictly prefer the crashing behavior here to help illuminate the use case scenario?

I worry that this won't be sufficiently clear: particularly in Foundation, where (for example) an "autoupdating" time zone value tracks something that can change from under you as long as the value is not mutated.

This parameter specifically takes something that is not a self-updating index and spits out a different one that is also not a self-updating index—and indeed understanding that is key.

I'm not married to tracking as an alternative. With pointers we have rebasing for a somewhat related but distinct operation. So, I suppose we could call this something like reindexing—I am also not married to this suggestion either and recognize there are reasons not to like it.

My point, though, rather than pushing any specific alternative, is that the API should stay away from the term "updating" because it specifically makes it blurry whether the thing is constantly updating itself or if you are doing the updating—the very distinction that matters.

I don't—but I'm worried in general about silently changing runtime behavior to be less stringent, particularly without an ergonomic replacement. Hyrum's law would dictate that crashing behavior is currently relied upon by someone.

1 Like

That's a fair point. I hadn't thought about the relation to "autoupdating" or something that is self-updating. I think that's mainly because I personally associate that behavior more with the "auto" part of the term than just updating. I still think knowing that I'd still lean towards updating over any other terms that I can think of like tracking/rebasing/reindexing because it does align with some other precedent we have in the standard library particularly in Collection-related APIs:

  • SetAlgebra.update(with:) which updates the set in-place (no -ing suffix) with the provided element
  • Dictionary.updateValue(_:forKey:) which updates the dictionary in-place with the new value for the key
  • Slice.update(repeating:)/Slice.update(fromContentsOf:) which update the contents of the slice with the provided elements

Those use "update" which I feel mirrors the "updating" used here to mean that the update returns the new, updated value so that's why I tend to lean towards this term as the best term I've come up with so far. But would definitely love to hear if others had confused this with an "autoupdating" behavior / if others have a good suggested alternative which I believe needs to convey:

  • The new value of the index is being returned rather than mutated in-place
  • The provided index value is the thing that is being updated
  • The index is updated with regard to what happens in the closure provided

Gotcha, that's fair and I'll definitely keep that in mind. The good news here is that this is actually roughly how AttributedString behaves today, with some caveats for edge cases. This really just formalizes this guarantee into part of the API contract that we must preserve rather than more of a "side effect" so I think the number of people relying on this behavior should be minimal (if any).

Thank you all for your feedback. There were no major issues raised during the review. There were some good questions about naming and behavior consideration. I'd like to accept this proposal providing that @jmschonfeld clarifies the behavior in the proposal.

Hi everyone!

After further discussion with some folks, I have a small revision to this proposal that I'd like to pitch. I'd like to propose adding both inout and Optional-returning variants of the transformation functions rather than just Optional-returning versions. This will allow developers to write code like the following:

var attrStr = AttributedString("The quick brown fox jumped over the lazy dog")
guard let rangeOfJumped = attrStr.range(of: "jumped") else { ... }
attrStr.transform(updating: &rangeOfJumped) {
    $0.insert("Wow!", at: $0.startIndex)
}

This will trap in the rare cases that the developer has performed an operation that cannot preserve tracking of indices while still allowing developers to pass the range as non-inout and receive an Optional return value to handle fallback behavior when trapping is undesirable.

I've posted an update PR to the proposal on the swift-foundation repo here. And would appreciate any thoughts or feedback on this revision. Please let me know if you have any input!

Thanks!

cc @itingliu

3 Likes