Hi everyone, thanks for all the feedback on SE-0377 and our other ownership-related pitches. Although the current review is nearing conversion, we've gotten a lot of feedback both on the forums and from early adopters with concerns about the annotation burden on code that wants to be ownership-aware while working with normally-copyable types, and that's guided our thinking about upcoming features in a way that may play back into the design of these parameter ownership modifiers. Particularly, we want to propose: should marking a parameter as borrowing
or consuming
prevent it from being implicitly copied in the function body?
borrow
, inout
, and consume
bindings
We plan to introduce new binding forms which allow for binding to a mutable or immutable value in-place within its original container, such as:
// Get a mutable handle to an element in a nested array of arrays
inout entry = &array[0][1][2]
// Perform multiple mutations on the value, without re-projecting it each time
entry += "foo"
update(&entry)
// Get an immutable reference to an element in a nested array of arrays, without
// copying it
borrow otherEntry = array[3][4][5]
// Perform multiple operations using the referenced value,
// without copying or re-projecting it
process(otherEntry)
doMoreProcessing(with: otherEntry)
One observation is that, if a developer has already made the choice to use one of these binding forms over let
and var
, they are probably doing so because they want to be aware of ownership and copying overhead. When a binding is explicitly borrowing something, it is probably surprising if the compiler is allowed to implicitly copy it if you try to do something consuming to the borrow:
func consume(_ value: Entry) {}
borrow entry = array[3][4][5]
consume(entry) // Would have to pass a copy of `entry`, not the borrow
So we're leaning in the direction that these bindings should not be implicitly copyable. We could avoid the need for a @noImplicitCopy
attribute for local variables altogether by also having a consume
binding form, which has ownership of its value like a let
or var
would, but which also:
- does not allow implicitly copies
- requires that the bound value is consumable without copying
- has "eager move" lifetime semantics, so its lifetime ends after a consuming use, or immediately after its last borrowed use if it isn't consumed
There is also a nice analogy between this consume
binding and the way the consume
operator from SE-0366 works, that parallels the relationship between inout
bindings and the &
operator, and borrow
bindings and the (to-be-pitched) borrow
operator. Each operator can then be thought of as expanding to a local scope that defines a temporary using the corresponding binding declaration, and passing the temporary as a parameter to the surrounding function call:
foo(&x.y.z)
// Can be thought of as shorthand for:
do {
inout temp = &x.y.z
foo(&temp)
}
foo(borrow x.y.z)
// Can be thought of as shorthand for:
do {
borrow temp = x.y.z
foo(temp)
}
foo(consume x.y.z)
// Can be thought of as shorthand for:
do {
consume temp = x.y.z
foo(temp)
}
As an alternative to @noImplicitCopy
as an attribute that you have to sprinkle all over your performance-sensitive code, we hope this design direction feels more integrated with the language. And it lets us give a more consistent story for working with ownership, whether with noncopyable or copyable types: if you want to (or must) think about ownership, use consume
/borrow
/inout
bindings; if you don't care, use let
and var
and have the compiler figure it out.
Relation to parameter ownership modifiers
We've tried to align the spelling of the ownership modifiers with the related operators because we want the relationship between the various possible calling conventions and the mechanics of ownership and borrowing to be clear. As such, to keep that consistency going, it would make sense to be able to say that if you declare that a parameter is borrowing T
, that the parameter ought to act like a borrow
binding within the body of the function, and likewise, a consuming T
ought to act like a consume
binding, and an inout T
as an inout
binding, including the lack of implicit copyability.
I had personally started from the position that these parameter modifiers should not affect anything except for the ABI of copyable parameters, either in callers or the callee, since it's important for libraries to be able to have the ABI changes as an optimization tool without imposing the disruptive need to think deeply about ownership on their clients. But other folks on the Swift team and community feedback have helped convince me that imposing copying constraints locally in the callee is likely a net positiveâchoosing to explicitly annotate your parameters' ownership already indicates an intent to consider ownership, and imposing the need for explicit copies on the modified parameter can serve as an indication whether changing the convention has the desired optimization effect in the body.
One wrinkle in this plan is that the inout
modifier already exists, and does not currently constrain the current value of the parameter from being implicitly copied, so we can't change its behavior without breaking source.
I know it's unusual to propose a change to a proposal relatively late in the review process, but I think this is an interesting design choice to consider that leads to a more consistent and integrated-feeling overall model for ownership. What do you all think?