As part of our ongoing work to bring non-copyable types to Swift [1][2][3][4], we've realized that there are a lot of core Swift constructs that imply copying, including variable initialization (let and var declarations), switch statements, and if let optional unwrapping.
To make these useful for noncopyable types, we'd like to add new "binding" or "reference" constructs that allow you to work with a value in various ways without any implicit copying. In the process of working out these ideas, we've realized that they can provide real benefits even for copyable types.
if inout y = &optional {
// `y` is a reference to the payload of `optional`
// So the following mutates it in-place
y += 1
}
// "&" below indicates `enum_value` might get modified
// It's required if there are any `inout` references in the
// switch statement.
switch &enum_value {
case .a(borrow x):
// `x` is a read-only reference to the payload
// so this calls a method on that payload
// without a potentially expensive copy of `x`
x.someMethod()
case .b(inout y):
// `y` is a read-write reference to the payload
// so this mutates the payload in-place
y.someMutatingMethod()
}
The benefits of this language feature extend beyond move semantics—this will make efficiently working with recursive enums (e.g. trees) significantly better!
i’m not a fan of inout y because i usually write swift with type annotations, and in my mind, inout is a storage modifier, not a variable declaration. so i would much prefer if the syntax was something like:
if var y:inout Wrapped = &optionalExpression
{
}
or
if var y:inout _ = &optionalExpression
{
}
for folks who don’t like writing out types as often.
Couldn’t we also infer the inout binding from the ampersand? Overall, I think var could make sense, and would mirror the current workaround of creating mutable computed variables (e.g. to modify collections in for loops).
I don’t think it’s very common to write var just to manipulate an enum payload that is not later stored back into the enum. Most current use cases of vars in enums are a workaround to not having an inout-like binding feature. Also, users will have to write &expression at the beginning of the switch statement, so I think users would rarely need to disambiguate. Even if there’s a need to disambiguate, ownership modifiers like borrow (or inout) could be specified in the type annotation:
i agree and i think with @filip-sakel ’s idea this would actually not be necessary, since we can infer inout from the prefixed ampersand. so it would just look like
if var y = &optionalExpression
{
}
which would avoid the need to use _ =.
but i really think we cannot use inout in the position where let or var occurs today, it just looks wrong. (to me anyways.)
I'm not enthusiastic about keying purely on the presence of & on the argument because it makes complex switch statements harder to read. Consider:
.... there's a `switch` way, way up there somewhere ...
case .a(var b):
print(b.methodThatHappensToBeMutating())
Here, I'm calling a method in order to get a value (but it happens to mutate something along the way, so it has to be var). Did we just change the enum payload or not? If we key only on &, then the only way to tell is to find a line of code far, far away from this and check whether it looks like switch value or switch &value.
In addition to the above, I think it would be good to introduce a single-line version that extends the alias to the end of the enclosing scope. C#’s using keyword has both variants:
// Scoped `using` statement
using (var file = File.OpenText('file.txt'))
{
// `file` is only bound inside this block
file.WriteLine('hello, world');
}
// Single-line `using` statement
using var file = File.OpenText('file.txt') // `file` is now bound until the end of the scope
file.WriteLine('hello, world')
In addition to the readability concerns, wouldn’t this be a semantic change? Pattern-matching with var against an inout parameter doesn’t implicitly give the var write-through semantics. You still need to reassign to the original inout binding:
enum E {
case foo(x: Int)
}
func f(arg: inout E) {
switch arg {
case .foo(var x):
x += 10
arg = .foo(x) // without this line, `x` never gets written back to `arg`!
}
}
I think it makes sense to keep var for new mutable variables, and use inout to declare an alias.
Bindings that imply "no-implicit-copy" should implicitly pass-by-borrow whenever possible. This is the same behavior that we'll expect for non-copyable types.
In the "Not implicitly copyable" section, this should be legal:
func normalUse(_ a: A) { ... }
normalUse(x) // Legal; callee convention is compatible with a borrowed value
The default calling convention is compatible with pass-by-borrow. The callee views its value as borrowed, regardless of whether the caller created a temporary copy.
Making this case illegal wouldn't serve any purpose and would add considerable complexity to the implementation. Today, these two functions have the same signature:
func normalUse(_ a: A) { ... }
func borrowingUse(_ a: borrow A) { ... }
If the calling convention actually requires a callee-side copy, then we should indeed raise an error:
func consumingUse(_ a: consuming A) { ... }
consumingUse(x) // Illegal; callee requires its own copy of `x`
Workaround:
consumingUse(copy x) // Illegal; callee requires its own copy of `x`
I believe willSet and didSet be called after case .a. Isn't it possible to only have inout reference in the case of case .b, like this?
switch enum_value {
case .a(borrow x):
// `x` is a read-only reference to the payload
// so this calls a method on that payload
// without a potentially expensive copy of `x`
x.someMethod()
case .b:
guard case .b(inout y) = &enum_value else {
fatalError("Unknown error")
}
// `y` is a read-write reference to the payload here
// so this mutates the payload in-place
y.someMutatingMethod()
}
This is not really the same as the using declaration in C# but that is only because you compared the using statement to an optional binding which isn't really the same either.
func foo() {
var myStruct = MyStruct()
inout alias = &myStruct.someProp
// from now until the end of the function,
// myStruct is inaccessible because it is being exclusively accessed
// via the alias to its someProp property
}
From the proposal draft, I get the idea that forming multiple borrow or inout bindings to members of the same struct value is prohibited if at least one of the bindings is inout:
struct T { var a: A; var b: B; var c: (A, B) }
var t: T
do { // Legal: multiple borrows
borrow a = t.a
borrow b = t.b
borrow b2 = t.b
}
do { // Illegal: inout and borrow of one property
inout a = &t.a
borrow a2 = t.a // 🛑 Exclusivity violation
}
do { // Illegal: borrow and inout of one property
borrow a = t.a
inout a2 = &t.a // 🛑 Exclusivity violation
}
do { // Illegal: inout and inout of one property
inout a = &t.a
inout a2 = &t.a // 🛑 Exclusivity violation
}
Is that right? How about tuples?
do {
inout (ca, cb) = &t.c
}
Is there any way to allow temporarily splitting the value into multiple distinct inout parts without any of the examples below being an exclusivity violation? With some new syntax, if need be?
do { // Could this be allowed?
borrow a = t.a
inout b = &t.b // âś…âť”
}
do { // And this?
inout a = &t.a
inout b = &t.b // âś…âť”
}
Sometimes I wish there was a way to pluck multiple properties of a value into a tuple value with a syntax something like t.(.a, .b, .c.0) for a tuple of type (A, B, A) or t.c.(ca: .0, cb: .1) for the tuple type with labels (ca: A, ca: B). Maybe something like that could be used to pluck out multiple inout bindings at once without an exclusivity violation, as long as the members are distinct in memory?
Inout and borrow becoming declaration keywords seems to say that it could have been better to have it on the identifier side rather than the type side in function prototypes. Is there room at this point to allow them on either side, with the intent of deprecating right-side inout/borrow at some point in the future?
The current proposal requires inout variables to be initialized at the site of initialization for simplicity. I imagine that it doesn't allow using the ternary or nil coalescing operator either, but that doesn't seem to be spelled out:
// probably not legal?
inout x = foo ? &int1 : &int2
// probably not legal?
inout x = &int1 ?? &int2
Those examples all look correct to me. But note that the following case is perfectly fine:
do { // Legal: borrow of an inout binding
inout a = &t.a
borrow a2 = a // âś…
}
At face value, this proposal implies these cases are illegal, but we could handle simple cases like this as a convenience the same way that exclusivity rules are currently implemented... Use the shorthand when it works. If the compiler gets confused, then fall back to an inout for the whole aggregate.
(We should at least mention the possibility in future directions.)
This should be implementable by the same reasoning as the struct case above.
For the purpose of this proposal, I think whatever cases can be handled by the implementation will just work with standard syntax. The new syntax sounds like it needs it's own proposal.
These new binding keywords express a programmer's intent to reduce the copying of certain values. As such, the symbols expressed by the keywords prohibit implicit copying.
If we want inout bindings to have "no-implicit-copy" behavior, then we should use a different name.
The borrow and inout bindings are bundled into one proposal because they are both "value references" with exclusivity scopes. But they aren't necessarily both "ownership controls". borrow is more akin to consume as a means to control value ownership and prevent implicit copies. I expect borrow and consume to have the same no-implicit-copy semantics at their uses regardless of whether they are parameter modifiers or local bindings. See SE-0377 borrow and consume parameter ownership modifiers.
The current inout parameter modifier does not exercise ownership control by preventing implicit copies. We can't change this without widespread source breakage. And variables with identically spelled declarations should have the same semantics at uses, regardless of whether they are parameters or locals.
Eventually we'll want a way to add ownership controls to inout parameters. At that point, we may want to introduce something like a ref parameter modifier. Should we anticipate the keyword we want for parameters and introduce that now instead of an inout local binding? Then we could give the new keyword ownership control semantics.