Pitch: `borrow` and `inout` declaration keywords

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.

The full pitch with more complete details is in this gist where I can edit it to incorporate people's feedback but the core idea is to add new uses of borrow and inout keywords that allow you to write things like the following:

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()
}

I look forward to the discussion!

40 Likes

The benefits of this language feature extend beyond move semantics—this will make efficiently working with recursive enums (e.g. trees) significantly better!

18 Likes

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.

9 Likes

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).

4 Likes

How would you handle switch statements?

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:

Big +1! This (along with other recent pitches) is bringing Swift closer in line with some of my favorite parts of Rust.

I find this syntax to be pretty undesirable. I tend to think that if we can avoid using _ =, then we should.

11 Likes

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.)

5 Likes

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.

2 Likes

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.

2 Likes

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`
2 Likes

I definitely like the feature.

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()
}

Do you mean something like this:

guard inout y = &optional else { return }

y += 1

?

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.

I mean this:

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?

do { // Future direction?
  inout (a, b, ca) = &t.(.a, .b, .c.a)
}
1 Like

I think that this is a very needed capability.

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?

func foo(x: inout Int)
// becomes:
func foo(inout x: Int)

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
1 Like

In a sense, spelling it out is unnecessary because those operators are never l-values in Swift. But I agree that it would be better to be explicit.

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.

1 Like

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.

2 Likes