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 else
s 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.
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")
...
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.
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:
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?
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
.
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.
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.
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:
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 sync
ed 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 normalset
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:
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.