Pitch: `borrow` and `inout` declaration keywords

That's a shame; the prime benefit of having them both use the same keyword, IMO, is that it conveys semantically that essentially the same thing is happening in both cases, save for the mutability.

I'm thinking about Rust's use of it, where the equivalent of Swift's (using proposed syntax)

let n = 37
var m = 42
borrow a = n
inout b = &m

...would be

let n = 37;      // n: i32
let mut m = 42;  // m: i32
let a = &n;      // a: &i32
let mut b = &m;  // b: &i32

Although the syntax makes it easy to conflate where the borrows are, I like that in Rust's case, we have this sort of 4-way matrix of mutability on one hand and ownership on the other.

owned borrowed
immutable let let _: &_
mutable let mut let mut _: &_

The proposed Swift syntax in the pitch is certainly simpler and less jargony, which I like, but it also doesn't quite convey this matrix as well, which I don't like.

owned borrowed
immutable let borrow
mutable var inout

So even if we don't use borrow as the modifier, I personally would love if the keyword for a mutable "borrow" were the same as an immutable one, so the matrix could look something more like this (replacing borrow with something else, perhaps.

owned borrowed
immutable let borrow let
mutable var borrow var
4 Likes

It seems really strange to factor discussion of borrowing declarations out from that larger feature, actively encourage people to propose spellings for it, and then a day later announce the foreclosure of parts of that design space.

1 Like

It's a difficult issue to solve; it's only natural to split up huge chunks of work into smaller pitches.

However, I do hope that the core team is open to revising proposals after they've been accepted as other related proposals come in before the next major Swift release, as I think it helps to look at these related features as one holistic set of work, such as is the case here.

3 Likes

I don't believe John was attempting to 'foreclose' anything, but it is the case that with the acceptance of borrow/borrowing parameter modifiers there will be a (short-lived) rule that borrow always means 'immutable'.

I also don't really think it's likely that the specific choice of borrow for SE-0377 is particularly material here: we probably want alignment between the "immutable borrow parameter modifier" and "immutable borrow declaration modifier," and it's a valid point that then further reusing the same keyword for "mutable borrow declaration modifier" may introduce more conceptual complexity for what 'borrow' means in the language as a term.

1 Like

Wouldn't it instead be:

let n = 37;      // n: i32
let mut m = 42;  // m: i32
let a = &n;      // a: &i32
let b = &mut m;  // b: &mut i32 <-
3 Likes

I think it’s pretty important that the pieces feel like they mesh and layer well from the outset. Swift’s adoption level is too high nowadays for big spelling changes to be tolerated, even in major language revisions. Trying to retroactively change anything about sendability produces serious frustration.

By the way, the chart you drew up reminds me very much of my own chart from back in December.

Hah, fair. :smiley:

Probably! Rust is only a very casual hobbyist interest for me at this point. :sweat_smile:

1 Like

Oh I'm definitely not proposing retroactive changes, at least not to anything that's been released in i.e. Swift 5.7, only that perhaps we can make revisions to proposals that are accepted but have not yet made it into a major Swift release.

Ahh, that is quite similar! Definitely thinking along the same lines, it seems.

1 Like

We're definitely open to changing accepted proposals that aren't yet part of an official Swift release if there's additional information that we should consider. However, we did have an extensive review of those proposals, and we considered a lot of alternatives, and we do need to make progress eventually. So there is a certain burden that has to be met before we're going to revisit these things.

I am well aware of how the term "borrow" is used in Rust, but I don't think we have to consider Swift completely beholden to that precedent, especially because borrow is not even a keyword in Rust. And I think having to use compound keywords everywhere we want to express references to storage just to maintain that precedent is a pretty high price. Assigning borrow to mean what Rust calls an immutable/shared borrow, especially when we have an existing term in the language for what Rust calls a mutable/exclusive borrow, seems like an acceptable decision that still pays respect to what Rust has done.

9 Likes

I can certainly respect that; mostly I just wanted to clarify what the policy was.

To be clear, I brought up Rust only because it was the closest existing precedent I could think of that generally fits my mental model of what's being proposed, not because I think Swift should model its syntax entirely off of it.

I personally find the compound keyword version preferable not because of prior precedent, but because I personally think it conveys what's happening more simply & clearly, and I find the addition of both inout and borrow to be just slightly more mentally taxing.

This is all just my opinion of course, and honestly it's a pretty minor gripe. Overall I'm excited about the direction, which is why I've participated in this probably more than any other proposal so far. :sweat_smile:

2 Likes

Right, I feel like there’s a couple ways to look at this. I like the re-use of borrow for the mutable/immutable cases because it conceptually links the ownership relation as the ‘thing’ that the two different versions share. This parallel didn’t exist for the borrow/consume parameter modifiers because the ownership relation for those two keywords were different.

I take the point that we already have a keyword for mutable borrow, but I don’t consider myself overly pleased with the inout terminology. It makes some amount of sense as a parameter modifier where there’s already a strong metaphor of passing values in and getting values out of a function, but I think it strains the metaphor somewhat when extended to local ‘inout’ variables. I’m not sure this outweighs the added complexity of an entirely new specifier when inout is ‘right there,’ but I think it’s worth considering.

One way to avoid the compound keywords issue everywhere while also maintaining some of the ‘rule’ about borrows being immutable would be to invert the parameterization so that you would say borrow(var) x: Int = &…, and the rule would then be borrow is mutable immutable unless explicitly specified otherwise. This would maybe also suggest allowing borrow(var) as a synonym for inout in parameter position. But this is likely also coming from a Rust-inclined thought process. :sweat_smile:

6 Likes

How about:

borrow x => let & x
inout x  => var & x
consume x => var && x
2 Likes

i like the idea of borrow(var), but i think immutability should be the default, because i think borrow(let) is going to be a lot more common than borrow(var).

Sorry, yes, that’s a typo. The rule would be immutable unless explicitly specified otherwise.

I'm curious what folks think about these as a possibility:

owned borrowed
immutable let borrowing
mutable var mutating

I like how these naturally fit into a discussion of "borrowing references" or "mutating references".

4 Likes

Now that we’re talking about borrows/aliases independently of implicit copies, I’m not sure whether the lack of brevity will affect me in practice. I do like mutating as a keyword—the existing meaning is naturally retconned as “shorthand for mutating self”.

IMO, borrow var is a little ambiguous. It could be read as implying that you may reassign the reference itself to point somewhere else.

That said, I definitely think we're stretching inout to a place where it no longer makes sense, and I would welcome a new name for mutable borrows. For instance, I'd like to see functions gain the ability to return mutable borrows one day:

var foo: Resource {
  get { ... }
  modify {
    var view = Resource(storage: consume(self).storage)
    defer { self.storage = view.storage }
    yield &view
  }
}

// Should also be possible with methods. Strawman:

mutating func foo(param: SomeParameter) -> inout Resource {
  var view = Resource(storage: consume(self).storage, param: param)
  defer { self.storage = view.storage }
  yield &view
}

Except inout doesn't really make any sense here; we're not passing a value in and getting it back out, but rather getting a variable out when this coroutine yields and passing it back in when it resumes to run its cleanup - more outin than inout, if you will.

Then again, maybe this kind of function (a yield-once coroutine) would just be spelled differently to make this clear in other ways. I could imagine replacing -> with a double-ended arrow, for example. Something like:

mutating func foo(param: SomeParameter) <-> var Resource {
  ...
}
3 Likes

Yes! This is why making 'inout' no-implicity-copy will break source by implicitly borrowing arguments. The violation is likely to show up when passing a value as an argument and passing a closure that captures the same value. Maybe that breakage is worth it under a Swift 6 mode.

do { // Illegal: simultaneous borrow and capture of an inout binding
  inout a = &t.a
  takeBorrowedArgWithClosure([borrow] a) { _ = a } // 🛑 simultaneous access
}

Note that there's no actual modification, but we capture 'inout' variables as mutable values regardless of whether they're mutated in the closure.

Personally, I find the meaning of inout clearer than &var. The value goes in to the local variable, and at the end of scope goes out back to its original position. Same as a function call. It sounds fine to me.


I'm less sure I like borrow in this position. For one thing, the word feels a bit too long to me for a variable declaration. For another, this:

borrow book = swiftAdventuresBook
borrow sameBook = swiftAdventuresBook
// Two people are borrowing the same book simultaneously (what?)

Basically, what I mean is that saying you're borrowing something sounds a bit like exclusive access.


If you want to convey that many people can get their hand on the same book at the same time, wouldn't this work better?

hold book = swiftAdventuresBook
hold sameBook = swiftAdventuresBook
// Two people are holding the same book simultaneously

You can also interpret the value as being "on hold" here, as an explanation of it not being modifiable. Or that you're "holding it in place". Or explain that you can't turn the pages when many people are holding the same book.


I also have to say this: people in this thread talking about inout being a mutable borrow vs. borrow being an immutable borrow is making me dislike the word "borrow" for an immutable borrow. It doesn't feel like borrow should sit side by side with inout. This table sounds better to me:

owned borrowed
immutable let hold
mutable var inout

I get that "borrow" is a term of art, but if the meaning of this term of art means both mutable and immutable, then we're not using it correctly by filling a single cell in that table with it.

5 Likes

borrow makes more sense in the context where it was originally conceived: a function call “borrows” its argument. Still, a caller can provide the same value for two arguments, so that doesn’t alleviate the potential surprise about multiple simultaneous read only borrowings, but that’s part of the model that people will need to learn.

I’ve used the term “alias” a few times in this thread. How about that?

owned borrowed
immutable let alias
mutable var mutating alias

Does this discussion impact the syntax for terminating the lifetime of a borrow/alias? The most recent revision of SE-0366 spells this as _ = consume myAlias. If borrowing no longer implies a jump to move semantics, is consume really the ideal keyword? What about the more concise _ = &myAlias?

4 Likes