borrow
will still be the default in almost all cases, so you shouldn't have to write it much. The alternative to borrow
isn't pass-by-copying, it's pass-by-ownership-transfer (what we call take
in the proposal). Whether the caller makes a copy or not is an orthogonal decision made by the caller, and there are reasons we'd do so for both borrowed and taken parameters—to avoid aliasing writes to shared mutable state in the former case, or to preserve the lifetime of a value the caller either still needs to keep around for itself, or doesn't have ownership of in the first place, in the latter. Stay tuned for future proposals if you want to hear about controlling or preventing implicit copies.
borrow
will still be the default in almost all cases
Huh, maybe I have misunderstood all along. I didn't think borrow
was currently the default; my understanding was that retains are omitted but for small types, bits might be copied. I guess that's not distinguishable from borrow
for copiable types which I guess is what I was saying.
so you shouldn't have to write it much
So the only place you'd be writing it is, e.g. for initializer parameters that you don't want to consume?
That sounds pretty good!
to avoid aliasing writes to shared mutable state in the former case
Sorry, I don't quite see how that plays out. We're talking about copies implicitly generated by the compiler, but a borrowed
variable is immutable, so you can only mutate a different variable; if there's a copy involved it seems like it would have to be made explicitly by the user. And if you mean it's sharing state behind-the-scenes, no amount of copying will avoid the aliasing. Could you explain?
to preserve the lifetime of a value the caller either still needs to keep around for itself, or doesn't have ownership of in the first place, in the latter
That makes total sense; it's a place we'd be telling the user to make an explicit copy in Val (unless they turn on @implicitcopy
). Maybe the models are closer than I thought. /cc @Alvae
You are right that, from the callee's perspective, the borrowed parameter value is always an immutable value (well, shared-borrow, which is the same most of the time). The "do we retain/copy or not" negotiation happens primarily on the caller side, though. If the argument is known to untouched by anyone else for the duration of a call, then the caller can pass its value along without any copies or retains, which should happen for unescaped local variables, parameters, and let
constants:
class Foo {}
func callee(_: Foo) {}
let globalLet = Foo()
func caller(param: Foo) {
let local = Foo()
var localVar = Foo()
// We shouldn't need to retain/release for any of these calls:
callee(local)
callee(param)
callee(globalLet)
callee(localVar)
}
If there's a potential for the argument to be modified during a call, though, then we have to copy or retain in order to safely maintain the illusion of pass-by-value. This happens with mutable globals and class ivars, since we don't usually know whether the callee turns around and calls something that touches those, as well as if you try to pass a mutable variable by-value and by-inout simultaneously:
class FooBox {
var foo: Foo
}
var globalVar = Foo()
func mixedCallee(_: Foo, _: inout Foo) {}
func caller2(boxParam: FooBox) {
var localVar = Foo()
// This has to retain, to resolve the exclusive/shared conflict
mixedCallee(localVar, &localVar)
// These probably retain (but might not with enough effects analysis…)
callee(boxParam.foo)
callee(globalVar)
}
For copyable types, borrow
vs. take
on the parameter doesn't have a huge effect on whether these copies are necessary or not; take
introduces the ability to forward ownership for the last use of a value, but conversely, requires a caller to also copy for any non-final uses of the value. In Swift, we've already made our bed more or less with how copyable types work, so the way I see to eliminate these kinds of implicit copies is to provide ways for the code in the caller to opt out of implicit copying, and require explicit copies in situations like mixedCallee(localVar, &localVar)
that obviously violate exclusivity without a copy, and/or specify that we aren't concerned about aliasing writes to objects or global variables at certain call sites, and it's OK to borrow their values in place (and trap if someone does come and try to rewrite the value during the access).
+1 This looks good to me. I like the new borrow
/take
/taking
naming scheme.
I’m generally in favor of this and can already think of a few specific locations where this would improve performance in a meaningful way (when coupled with take
expressions), even without any further language changes. Also very excited about the general progress towards move-only types.
Inside of a function or closure body,
take
parameters may be mutated, as can theself
parameter of ataking func
method.
I feel like this isn’t necessarily justified; while it is just as reasonable to mutate a take
parameter as a local var
, I don’t think it makes sense for that to be the default. With locals we still encourage let
over var
, and take
doesn’t imply “mutation” to me. Certainly the implicit uses of the take
convention today aren’t about mutating the value; they’re about storing it. Similarly, the use case I have in mind is purely about allowing the parameter to be destroyed sooner, avoiding having to wait for a deep stack to return.
I suppose you can always convert from one form to another by take
ing into a local, but I don’t think take
should imply mutability like var
and inout
do. I’d rather see a (separate) proposal to revert SE-0003 and revive var
on parameters if we think this is interesting, noting that SE-0031 moved inout
to the “type” side of the parameter declaration since SE-0003 was reviewed.
take
cannot be applied to parameters of nonescaping closure type, which by their nature are always borrowed
The full form of a non-optional take
closure parameter would thus be callback: take @escaping () -> Void
, which is pretty verbose. (Or is it @escaping take () -> Void
?) Given that escaping pretty much implies that a copy will be made anyway, does it make sense for take
to imply @escaping
, as inout
already does today? That still leaves the occasional borrow @escaping () -> Void
for where an escaping closure parameter might normally be take
n, but that seems pretty rare to me; the main place where I could see it being useful is if the closure is only escaped some of the time.
On the implementation side: are non-escaping closure parameters passed borrow
or take
to initializers today? I would hope borrow
even though the usual convention for initializer parameters is take
, for the reasons discussed in this proposal, but if not this could be a good opportunity to fix some conventions (where not ABI-breaking).
Additionally, I remember the first implementation of __owned
and __shared
affecting a function’s mangled name even if the convention matches the implicit default for that parameter. Does that behavior extend to take
and borrow
, and if not…I guess there’ll be a special opt-out or shim for the stdlib, to use the explicit mangling where an implicit one would have done?
Again, very excited about all this! Thank you Michael, Joe, and everyone working on this.
I am +1 on this proposal. But I have one open question about closure capture lists. Is it possible to have take/borrow
modifiers on capture list elements? We had this discussion over in this thread where we were talking about a safe way to pass AsyncIterator
s between Task
s while retaining the 1:1 relation ship.
I agree with this, the default should be borrow and Swift code with the borrow keyword everywhere would be unbearably ugly. However, don't we need some attribute declaring if it is a mutable or immutable borrow or is this covered by inout? Take passing is more rare and you also appreciate that badging because then you know that the function will take hold of that resource.
Is there a way to declare per function or even entire files that you want to use the borrow/take feature?
struct Foo { taking func foo() // `take` ownership of self borrowing func foo() // `borrow` self mutating func foo() // `modify` self with `inout` semantics }
Yuck!
This looks much better:
struct Foo {
func foo() takes // `take` ownership of self
func foo() borrows // `borrow` self
func foo() mutates // `modify` self with `inout` semantics
}
Though that is better aligned with other function modifiers (such as async
and throws
), it's worth noting that mutating
with the former syntax already exists in the language, so to do that would be a breaking change. If the first syntax even becomes so much as a warning, we may see libraries having to do this:
#if swift(>=5.8) // or whatever version
func foo() mutates {
// ...body...
}
#else
mutating func foo() {
// ...same body...
}
#endif
So I'm not sure that would be worth it.
This would introduce inconsistency with the fact that async
and throws
are keywords for denoting effects and already have their own set of "rules". Both are parts of their type signatures, (Int) -> String
and (Int) async -> String
are different (even though they have a certain subtyping relationships), while (Int) mutates -> String
does not make sense as a standalone type signature. Effectful read-only properties were introduced in SE-0310, allowing get async
and get throws
in property declarations. In the light of that, what would get mutates
even mean?
Additionally, throws
has counterparts in throw
and rethrows
. async
has await
and potentially reasync
, as discussed in future directions of the corresponding proposal. I don't think mutate
and remutates
make sense even at a conceptual level. In this sense, taking
, borrowing
, and mutating
are not function/property effect modifiers.
Rust’s notion of “mutable borrow” does indeed correspond to Swift’s inout
. They’re slightly different (and so are the two “borrow”s) because Rust’s references are first-class values that promise a pointer representation and Swift’s are calling conventions that do not expose the representation. (For example, a borrow
ed Int in Swift will still be passed by value because it is a small frozen trivially-copyable struct.)
Isn't mutating get
already a thing? I'm 90% sure that lazy
variables have an implicit { mutating get set }
attached to them.
Yes, that one's a weird special case, but note that it's then consistent with mutating func
. It still stands far from effect keywords in all other aspects.
If we want a consistency argument, mutating
and the new proposed keywords affect how self
behaves within the function, while also changing the self
the function is called on. What other modifiers on functions affect self
like this? static
/class
, and maybe convenience
as a stretch. These also appear at the start of the declaration.
(I don’t put a lot of stock in consistency here, though, cause it’s clear that “before the declaration” is the default place to put modifiers in Swift, and throws
and async
are the unusual ones.)
Since mutating get
was brought up: what about nonmutating set
? Would that be respelled borrowing set
now?
Similarly, would it make sense to allow taking get
or taking set
?
Edit: it seems the compiler doesn't currently reject __consuming get
and __consuming set
. It even allows __consuming willSet
and __consuming didSet
, though I'm not sure why you'd use those.
Do we want to allow setters to borrow the new value? Currently, a setter can't mark its argument as __shared
or __owned
. However, allowing that opens up a whole can of worms:
- What if a
set
,willSet
, ordidSet
tries to have aninout
argument? - What does it mean for
didSet
to take its argument (oldValue
) by anything other thantake
? - Should
willSet
be allowed totake
its argument (newValue
)?
Maybe this was already discussed and I just missed it, but willSet
and didSet
don't seem to be mentioned in the proposal at all.
That's an interesting question. borrowing set
would indeed be equivalent to nonmutating set
, but we should probably keep the existing spelling working for compatibility. As you noted, __consuming
is currently allowed on get
and set
accessors, and I would say taking
and borrowing
ought to be allowed as well. It doesn't really make sense to allow them on didSet
or willSet
since those get absorbed into the setter and modify
coroutine ABI-wise, and they always follow the ownership semantics for setting a stored property of the containing type (which would be mutating
for a struct or borrowing
for a class).
It might be interesting to allow set
to borrow or inout
its newValue
, but I think that doing so requires further language design to work out what that means in relation to the borrow/inout access on the base of the property being set. (Similar issues cloud our ability to put throws
on set, for instance.) So I think it makes sense to explicitly subset setters out of this initial proposal. Since willSet
and didSet
are tied to the default assignment behavior of stored properties, I don't think that modifying the semantics of oldValue
/newValue
is particularly useful.
I was always in favour of prefix spelling for async/throw as well:
async func foo() -> Int {}
throwing func foo() -> Int {}
but OTOH as mentioned above async/throw is part of function signature so I am not so sure about it.
1
Taking is an action. Owning is a state. Taking means a transference of possession.
If an owned object can be borrowed, then an owning function is a contradiction in terms, because it explicitly cuts off this possibility: 1. by scope 2. by voiding the argument.
2
Actually, the term of borrowing is also not quite there. In actuality, how many borrowers have access to the same thing? How can multiple borrowers get the same thing?
Only 1. by reproduction, i.e. copy onto parameter or 2. by glimpsing into its argument in place, not moving or copying.
Only single-access borrowing should actually be considered borrowing.
(Considering that its intention is to change the value, it’s not perfect as well. You wouldn’t expect a thing you lend to return changed. The contract is usually precisely that trust is given to not change it. But at least changing it is not precluded.)
3 a musing, not even a suggestion
I myself enjoy a different analogy – that of flows. Terms like conduit, flow, tapping, filling.
One could imagine the value as a quantum of energy. Then borrowing is tapping, and mutable borrowing is switching or swapping. Or mutating.
I like the similarity of Take and Tap and how it separates itself from mutation in situ, which is the most special case and the main antagonist.
(I also like the mut
of rust, as it does not only mutate, but ‘mute’ the value for the time being – not other place can have it). But it’s too poetic for the other tastes.
Whatever modifier we choose to put in (front of?) the function, I think mutating
needs to match inout
. If we go with a naming scheme like takes
, borrows
, mutates
then an inout
parameter should also be spelled mutate
. Because as it currently stands, take
is a verb, borrow
will be a verb and inout
will have an adjective role.