@Joe_Groff suggested some possible alternate keywords we could consider:
borrowing x = a.b // Instead of `borrow x`
mutating y = &c.d // Instead of `inout y`
@Joe_Groff suggested some possible alternate keywords we could consider:
borrowing x = a.b // Instead of `borrow x`
mutating y = &c.d // Instead of `inout y`
i’m not opposed to Joe’s idea. but does it mean we could write:
mutating y:inout [Int] = &c.d
?
Indeed. I was in the middle of posting this...
What we really have in Swift are styles of variable bindings. They need to be written in type position sometimes because they affect calling convention.
We want to introduce new bindings that have the same semantics as noncopyable types (except that you can explicitly "copy" them).
Implicitly copying bindings: let/var/inout
Noncopying bindings: borrow/consume/mutate
I'm still concerned that such syntax could be read to imply that inout
means "pointer/reference to". This pitch is not trying to introduce a notion of references as first-class objects. In particular, the &
sigil here is not an "address of" or "reference creation" operator; it's just a reminder that the object on the RHS of the =
sign may be modified by later code. (In this respect, it does serve the same role as when passing a value to an inout
function parameter.)
Also, if we avoid using the keyword inout
for bindings, then inout
would remain in the language only as a modifier for function arguments, which is not relevant here.
It just occurred to me that your real question here may be about how type annotations should work with such syntax?
What about this?
mutating y: [Int] = &c.d
My internal mental model (which may not be totally accurate) is that both inout
and borrow
in this pitch are referring to aliases to other values; the difference is that one is mutable and one is not. There seems to be an obvious parallel to var
and let
here; one you can mutate and one you cannot. The new thing in the proposal is the borrowing/aliasing/etc behavior. So rather than have two "new" keywords in declarations, why not simply call out the borrowing-ness of it? E.g.,
borrow let foo = bar.x
borrow var fuz = &bar.y
As somewhat of a precedence, we already have async let
as an "effectful" declaration.
i’m more approaching this from a “knowledge transferrence” perspective for this new feature. correct me if i’m wrong, but my understanding of this new feature is it has very similar semantics to an inout
function parameter, so i feel that it would be easier to understand if the syntax to declare an “inout
” local variable looked more like the syntax to declare an inout
function parameter.
because if i were to see something like
i would try to understand that by imagining what it would look like if it were declared as part of the function signature instead of as a local, so i would mentally translate it into:
(var) y:inout [Int] = &c.d
just like we do
foo(x: &c.d)
func foo(x:inout [Int])
{
}
of course if i am wrong and this is really only a superficial similarity, then we should not reuse syntax, we should have a separate syntax like mutating y:[Int]
. but if it is the same concept we should not have two completely different ways of writing the same idea.
async let
isn’t really a good role model to emulate, because it didn’t account for anonymous-but-cancellable tasks, like we often want when using asynchronous APIs with timeouts. notably, there is no way to express the following in swift 5.7 without compiler warnings.
async
let _:Void = self.failRequest(id, by: .now.advanced(by: timeout))
return await self.runSomeArbitrarilyLongOperation(id: id)
because async let
does not support anonymous tasks (in swift 5.7).
Or to ‘borrow’ the syntax we use in other places for parameterized declaration modifiers:
let(borrow) x: Int = …
var(borrow) y: Int = …
We could also have explicit (copy)
variants that would be the default if nothing was specified.
This is true, I'm also currently mildly frustrated with some of the limitations of async let
, but I'm less concerned with how it works in practice and more just the semantics and aesthetics of the syntax. I like how async let
looks, and I like how borrow let
/var
looks.
I can see borrow let
, but I think borrow var
would be a serious mistake, because it muddies an otherwise consistent rule that borrow
means an immutable access.
Does such a rule yet exist?
You're going to make me spoil the SE-0377 announcement. Yes, the use of borrow
/ borrowing
for immutable reference parameters is going to be accepted.
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 |
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.
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.
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.
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 <-
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.
Probably! Rust is only a very casual hobbyist interest for me at this point.