Modify Accessors

yield is a new control statement, and it's behaviour doesn't exactly match anything Swift already has. The else in yield ... else has a different behaviour from the other elses because yield is a new control statement. I don't think it can lead to confusion because unlike if or guard, yield doesn't take a boolean condition, so it's meaning has to be different - it cannot be mistaken for anything else.

1 Like

There would be a strong implication that yield can somehow evaluate as false. The idea that else would mean something different than in virtually every single other language that uses the keyword, and different than the other usages in Swift itself, is sure to never cross anyone's mind. Swift should not use the same keyword to mean something completely different in similar-looking contexts.

Changing control flow is always a pitfall, and I don't think yield should do so without any hint what is (or might be) happening.
I don't have any good examples where the accessor should take special care for "failed" yields, so I prefer a simple (user perspective) model where control is always returned to the accessor, no matter what happens during the yield.

When (if) yield obtains other uses, we could adopt the guard let pattern:

modify {
   print("Going to yield")
   yield &someValue
   print("Done - maybe an error happened, but I don't care")
}

...
for value in values {
   guard yield &value else {
       print("Client doesn't want elements anymore, so we stop yielding")
       break
   }
   print("Moving to the next element...")
}
print("Done - maybe an error happened, but I don't care")
...
1 Like

We do! If an error is thrown by the code that interacts with the yielded memory, then the cleanup must be forbidden from throwing. And that”s what defer does.

4 Likes

Is modify itself allowed to throw at all?

Not yet, but throwing accessors are an expected improvement. We shouldn’t design anything that forbids them.

Some 2019 developments:

5 Likes

Very excited about this proposal and like the direction, though I lean towards agreeing with brentdax's arguments about control flow so far.

On a separate note, I'm not sure I understand the implications of the following

struct GetModify {
  var x: String =  "👋🏽 Hello"
    
  var property: String {
    get { print("Getting",x); return x }
    modify {
      print("Yielding",x)
      yield &x
      print("Post yield",x)
    }
  }
}

var getModify = GetModify()
let getModify2 = getModify
getModify.property.append(", 🌍!")
// prints:
// Yielding 👋🏽 Hello
// Post yield 👋🏽 Hello, 🌍!
print(getModify2.x)
// What is printed??

Does x still retain its value semantics therefore it's copied during the modify/yield since it is not uniquely referenced?

1 Like

x will still have value semantics. In your example, when you create getModify2 this will cause the contents of getModify to be copied. This causes getModify.x to be copied as getModify2.x. This will mean the backing buffer of getModify.x will no longer be uniquely referenced. So when you modify it, CoW will trigger. The (immaterial, in this case) difference is there will be 2 references to it when it is yielded, rather than the previous 3 when using get/set. Think of this as similar to what happens when you make a copy of a struct containing a string, then mutate the original with a mutating func that modifies x.

2 Likes

Are there any plans for adding modify behaviour to WritableKeyPath and the KeyPath subscript? Seems like a nice performance win we all get for free.

3 Likes

So I decided to add a modify accessor to the TupleCollection @jrose shared on twitter here a bit ago.

It didn't end up exactly how I expected so I am wondering would it be reasonable to allow yield in a non-escaping closures that are part of coroutines?

What I first tried was this:

_modify {
    let count = self.endIndex
    withUnsafeMutablePointer(to: &tuple) {
        $0.withMemoryRebound(to: Element.self, capacity: count) {
            yield &$0[position]
        }
    }
}

Which resulted in these errors with Xcode 11.3:

Consecutive statements on a line must be separated by ';'
Use of extraneous '&'
Use of unresolved identifier 'yield'

After realizing that yield must not be allowed in the scope of those closures I ended up with this:

_modify {
    let count = self.endIndex
    var movedValue = withUnsafeMutablePointer(to: &tuple) {
        $0.withMemoryRebound(to: Element.self, capacity: count) {
            $0.advanced(by: position).move()
        }
    }
    defer {
        withUnsafeMutablePointer(to: &tuple) {
            $0.withMemoryRebound(to: Element.self, capacity: count) {
                $0.advanced(by: position).initialize(to: movedValue)
            }
        }
    }
    yield &movedValue
}

I am fine with the use of defer for clean up as proposed in the pitch but think that this could potentially address the need of defer in a lot of cases making it a little simpler to use correctly.

2 Likes

A potential problem is that a non-escaping closure may run more than once, or not at all (for example, map).

True... I didn’t think about that. For generators that would be fine but modify would need to ensure it only happens once.

Does it need to yield only once? I don’t quite understand the mechanics behind it, but it seems to me that when a function is trying to modify a value (via its modify accessor for example) it may need repeated access to the value’s contents anyway. And assuming yield only yields to a single control point (a single caller), this should be fine. Cleanup can happen in defer.

That, or I’m misunderstanding the issue. Does the closure return control back to modify multiple times? That could be weird...

Yes. In the pitch:

1 Like

Okay. I must’ve missed that bit.

I guess that ousts closures, then. Is there any possibility that one might be able to yield onto a DispatchQueue? Or would plain threadlocking be necessary?

I mean, DispatchQueue only ever executes a synced closure once, correct? Might we add some sort of annotation to hint to the compiler that certain closures will only execute once?

I think this is a really interesting pitch and would like to see it move forward.
About the discussion about yield and defer, I think it's alright as it is.

  • I like yield not only because it says what it does, but also because is the common name used to refer to coroutines.
  • I know this is not a topic about async but if we introduce coroutines at the language level I honestly would like to see the same keyword on all features, including async. (imo async is too specific)
  • The usage of defer is quite clear once you understand what's going on. Is not obvious for a newcomer but honestly this feature is a little advanced IMO. You have the normal set to start with, where you don't need all this knowledge. That said, if we find an elegant way of improving this I'm all in :)

Also not sure if this has been discussed (sorry, >200 posts! :) ) by I'm more worried about this than anything else on the pitch:

2 Likes

@anandabits, could you please elaborate with an example of what you mean?

I noticed that Array's implementation of subscript uses only a _modify. Is there a reason why Array isn't taking advantage of the optimization of a set when performing straight assignments?

By its nature, array’s mutating subscript always involves an existing value at the position.

Late to the game but thanks, this is exactly what has been confusing me. I can't see the purpose of the coroutine concept here, when all you really do seems to be to allow callers to run a modification closure on your backing property.

I can understand that there are other reasons for introducing (semi-) co-routines, perhaps related to SwiftUI or (let's hope:) async/await, and perhaps I am missing something.