I think I like inout
for for inout
loops, even though the metaphor is more "out-in", it takes a value out of the collection and them puts it back in, but "out-in" looks wrong and inout
is already a keyword which is close enough.
+1, this seems like a great change! It's a bit unclear how this change will interact with result builders though.
I've made minor revisions to the pitch:
- Added @ksluder's "automatic
inout
" suggestion to the alternatives - Added a clarification about the supposed use of
defer
prompted by a discussion on Mastodon - Added a note to the Swift 6 alternative pointing out that we could change the default in a separate proposal
- Added a note to the section about borrowing/mutating iterators about library evolution limitations
- Moved some of the technical language from the motivation section into detailed design instead
- Expanded motivation section's discussion of the existing copying behavior
- Linked to pitch thread, now that it's up
- Prose fixes and formatting improvements
I haven't attempted to fully address some of the bigger discussions in this thread that don't seem to have settled yet:
- Copying a
Collection
and borrowing its elements - Borrowing/mutating iterators
- Unsafe indexing of
Collection
s
I very much agree. Whatever for
does here should match if
and switch
and so on.
This would be complicated to implement today because the code fragments the for
loop uses to iterate over its input are generated very early, in CSGen, and the type of the collection you're iterating over is not yet known at that point. (That used to be SILGen's responsibility, but Pavel moved it into Sema recently, I believe because it made it easier to support iterating over Collection
existentials.) That seems fixable, though, especially if we can defer it until non-copyable types are actually a thing.
Unfortunately, we can't make MutableCollection
refine this generator-based protocol, even if we provide a default implementation. I would love to find a good way around that issue.
Did you know that Swift supports computed local variables?
var x: TypeOfX {
get { foo.bar[baz].zot.x }
set { foo.bar[baz].zot.x = newValue }
}
I don't think it would do anything different from a normal for
loop—the results of each statement in the body would be passed to buildBlock(_:)
, and then the results of all the buildBlock(_:)
calls would be passed as an array to buildArray(_:)
.
I was thinking of it the other way around: We would have a default implementation of the generator-based protocol that could wrap an arbitrary MutableCollection. Then we could use index iteration (over MutableCollection) in the near term and migrate to the generator-based protocol in the future. That future implementation would use the generator-based protocol always -- either a custom implementation or the default implementation wrapping MutableCollection. Future collections could provide either one and still work correctly -- in particular, it would be possible for a library author to implement only the generator API and get for-loop support without having to implement all of MutableCollection.
This change, by the way, is breaking existing in-the-wild user code in certain corner cases—I'm trying to locate the extant bug but it's not coming up at the moment; by memory it has something to do with types where the protocol-conforming makeIterator
overload isn't utterable on the instance without casting to an existential box.
Besides that it's a less-than-ideal state of affairs, the related point for this thread is that we should try to ensure that the generated code for for borrow...in
, etc. (which by necessity may have to differ from that for plain for...in
) doesn't further result in additional use cases where something is iterable with one kind of loop but not another.
It seems like everyone is convinced this requires more keywords, so I doubt anyone is considering this, but I want to mention what I already said in the other thread.
for x in array {} // immutable for loop
for var x in array {} // mutable copying for loop
for x in &array {} // immutable borrowing for loop
for var x in &array {} // mutable borrowing for loop
Simplest possible and mirroring existing convention. Is there something only the keywords can do that justifies the added boilerplate?
You're taking ownership of the entire array, not individual elements, so &array
is more accurate than any variation of for inout x in array
.
It's not "for borrowed x in array", it's "for variable in borrowed array".
It wouldn't be a good idea to have overlapping mutable array access, unless the ownership system could figure out individual mutable slices and ensure they don't share any state.
Unless I'm missing something and it's fine to have multiple mutable references to an array
I very much agree with this sentiment. The features themselves are fine; I don't have much/anything to add to the discussion WRT to functionality, I don't think.
But I am slightly concerned about the syntax. Aesthetics and verbosity are factors, but more importantly, each of these concepts needs to feel like part of one coherent ownership story - if you know how one thing works, and you need something a bit different, you should be able to reasonably guess how that other thing is spelled. I'm not sure that the proposed keywords feel connected enough, and I worry that people won't pick it up as easily or that it won't be very intuitive for them.
If I have an algorithm which uses a for
loop, but now I want to accept collections of non-copyable elements, for borrow
is not an entirely obvious leap, IMO. If it then turns out I need to mutate those elements, going from there to for inout
seems even less obvious.
There have been several interesting ideas for alternative syntax in other threads, such as from @TeamPuzel, @JuneBash, and others. I'm not sure if I have a favourite, but I hope the language workgroup gives serious consideration to those suggestions.
I for one definitely didn't know this. Perhaps a bit OT but it would be nice if we had a way to define computed variables (and even subscripts) without having to repeat the underlying expression:
var x: TypeOfX {
getSet { foo.bar[baz].zot.x }
}
subscript(index: i) -> TypeOfX {
getSet { foo.bar[i].zot.x }
}
As to the topic, I love the concept but find at least the inout
notation a bit surprising, since it's usually not a usage site keyword. Shouldn't it be simply:
for &x in allExes {
x += 1
}
or
for x in &allExes { ... }
As mentioned above, this is tied to the question of what’s being borrowed: the collection, and/or each element within the collection?
Sure, I'm just saying that inout
doesn't seem to make sense here, unless I'm missing something.
My “instinct” wants to write things like this for the mutating version:
for &n in numbers {
n += 1
}
I don’t know enough about borrow semantics to weigh in on whether the collection and/or its elements should be implicitly borrowed. From a language-user perspective, it would be nice if the compiler did The Right Thing™ automatically, but again I don’t know what the right thing is.
Here's my suggestion:
var numbers = [1, 2, 3]
for alias x in numbers {
// immutable borrow
}
for alias y in &numbers {
y += 1
// mutable borrow
}
The types/bindings (e.g. when inspected in Xcode) would be:
var numbers: [Int]
alias x: Int
mutating alias y: Int
I agree that this feels natural, but it doesn’t necessarily make sense if you think about it too hard.
I feel like every discussion there’s been about borrowing has produced some subtle nudge toward Rust-like syntax. It all just seems to hang together more naturally, and it’s easier to predict the correct spelling of combinations of language features. For example, @JuneBash and I independently arrived at &
being the universal signal for borrow expressions, which is very similar to Rust.
But as @jrose pointed out, borrowing is so common in Rust that the pervasive use of &
feels like burdensome ceremony. That’s a good thing to avoid.
All that said, what if Swift 6 used &
as a sigil rather than an operator? It could be optional when implicit copies are permissible.
var lines: [String] = …
for var &line in lines {
// `&line` is the name of a mutable borrow
// this very clearly operates through the reference
&line += "\n"
// you can use it without `&` to implicitly copy (except for move-only types)
print(line)
}
Per the above discussion, lines
can be optimized to a borrow during the iteration if it’s statically proven not to escape the enclosing scope. To force a borrow of the whole collection, prefix it with &
:
for var &line in &lines { … }
Types are the immediate question—specifically anonymous function types. But it’s not uncommon for things to be “backwards” in that position (e.g. @Sendable
):
// currently proposed as
// func forEach<S>(_: borrow S, do: (borrow S.Element) -> Void)
func forEach<S: Sequence>(_ &sequence: S, do operation: (& S.Element) throws -> Void) rethrows {
for &element in sequence {
// including the & sigil is necessary
// to indicate we are passing the borrow
operation(&element)
}
}:
let strings: [String] = …
forEach(strings) {
// semantic copy without the &
// (possibly optimized to implicit borrow)
print($0)
// explicit borrow
doSomethingElse(with: &$0)
}
This doesn’t mean & String
is a true type—you can’t create an Optional<& String>
. Borrowing is an intrinsic property of bindings, not an aspect of their types.
This avoids the awkwardness of inout
in declaration position. I’m personally a fan of deprecating inout
entirely, because I think it’s dangerous that mutations which appear to be local are in fact silently happening through a borrow:
// In Swift 5.x, this is currently spelled
// func reverse<C>(_ collection: inout C)
func reverse<C: MutableCollection>(_ &collection: var C) {
// let’s imagine MutableCollection.reversed() returned one of these
if collection is ReversedCollection<C> {
// note & on LHS of assignment
// (also demonstrates lack of & in cast)
&collection = (collection as ReversedCollection<C>)._base
return
}
let indices = collection.indices
let reverseIndices = indices.reversed()
for indexPair in zip(indices, reverseIndices) {
swap(&collection[$0], &collection[$1])
}
}
If it helps Swift keep its safe-by-default behavior without additional ceremony, and as long as it’s possible to easily wrap a value in a move only type, I don’t mind typing &
.
I really like how this proposal avoids needing to make a new type of iterator, seems very cool that existing types can get this behavior for free, even if they come from binaries built with an older swift version.
Can the approach of producing a reference from a subscript be used outside of a for loop? For example:
if let inout ref = someObject[index] {
ref.someField = foo
}
Apologies if this has already been answered elsewhere. I skimmed the originally borrowing proposal but didn’t see any mention of how this is supported, if at all.
Rather than thinking of the inout
as being attached to for
, think of it as being attached to x
. The for
loop will bind the elements to inout x
instead of just x
.
OK but still, in other contexts inout
is never attached to the variable, and not even present at the call site. It's an annotation used in function declarations, on the type.
I suppose one might see a for
loop as something in between a declaration and a call site, but I personally prefer to see it as a call site, and for
as a special kind of (meta) function.
By the way, what does the $ in "$n
" mean in the desugared examples?
The borrowing/inout proposal does deal with this case. Remember that collections can implement their subscript operations with explicit accessors. For example, here is one of the subscript operations on the standard library Array implementation. The section on "Visibility of mutations" details how this works with get/set accessors and there's a brief mention later in that document (under "Future Directions") of how this might extend to work with (not-yet-standardized) read/modify accessors.
So in your example, if someObject[index]
were implemented with get/set accessors, then there would be an implied set
at the closing brace to write back the modified value. If that subscript made use of read/modify coroutine-based accessors, then it can be implemented more efficiently.
Out of curiosity: Any progress on this pitch?