This sounds like something that should be discussed more widely. For example, if there is agreement on this point, that would impact the proposal for existential any
.
This is just talking about using a keyword for this specific feature. I am not saying anything about other keywords/uses and whether or not it merits it.
I didn’t mean to construe your post as rendering an opinion on that proposal; I’m saying that it highlighted a potentially unresolved language design question, since the authors of that pitch came to the opposite conclusion about the utility of a new keyword.
For people who are uncomfortable with the name "move", would consume
work better? Rewriting the first example from the proposal:
// Consumes x: y's lifetime begins and x's lifetime ends.
let y = consume(x) // [1]
// Consumes y: since _ is no-op, we perform an actual release here.
let _ = consume(y) // [2]
useX(x) // error, x was consumed at [1]
useY(y) // error, y was consumed at [2]
I do feel "x was consumed" is an error message that requires less explanations than "x's lifetime was ended" or "x was moved".
Sorry for the delay in response, I was thinking about this and prototyping some things. So I was able to just add support for inout parameters on main: https://github.com/apple/swift/commit/e6faa3048855741db1744ed1ced26d83c3a2cf90. We were having some weirdness on the bots so I don't have a toolchain for you yet, but I hope to have one today soon in this PR.
But yea, so with that PR on main we now support:
mutating func appendNoError() {
let b = _move(self).buffer
let maybeNewB = maybeGetNewB(b)
self = .init(buffer: maybeNewB)
}
mutating func appendError() { // expected-error {{'self' used after being moved}}
let b = _move(self).buffer // expected-note {{move here}}
let _ = b
} // expected-note {{use here}}
sort of diagnostics. The reason why the error is on the last '}' is that I am treating inout verification just like var verification except I give the inout an extra implicit use on all function exiting instructions. That ensures that a valid value must be there before function exit.
I have actually found it to be very useful in catching me forgetting to re-initialize self along try/catch blocks when I was writing test cases, so I imagine it will be very helpful.
With that in hand, I am pretty sure I can handle the defer case as well and handle your test case. But I am hacking on that now. Once I am done with that I am going to update the proposal with these things.
Thank you for explaining—I agree that withoutActuallyEscaping
is another good comparison.
The difference, in my mind, between move
as-proposed and withoutActuallyEscaping
(as well as withExtendedLifetime
, and type(of:)
two other 'pseudo-functions' that have been mentioned) is that ultimately, they all behave basically like 'true' Swift functions: they accept a value, and produce an output in accordance with all the usual rules of Swift semantics, albeit with some additional type system support that can't currently be spelled in a function signature (e.g., to make sure that the argument to withoutActuallyEscaping
is of function type).
For instance, the fact that escaping function types have a subtype relationship with non-escaping types means that all of the following compile, even though they are a bit silly:
func f(g: @escaping () -> Void) {
withoutActuallyEscaping(g) { escapingClosure in
print(escapingClosure)
}
}
func makeF() -> () -> Void { {} }
f(g: {})
withoutActuallyEscaping(makeF()) { escapingClosure in
print(escapingClosure)
}
withoutActuallyEscaping({}) { escapingClosure in
print(escapingClosure)
}
Furthermore, potential future language enhancements could make withoutActuallyEscaping
much more expressible in the type system. E.g., with variadic generics the signature of withoutActuallyEscaping
could look something like:
public func withoutActuallyEscaping<Args..., InResultType, OutResultType>(
_ closure: (Args...) -> InResultType,
do body: (_ escapingClosure: @escaping (Args...) -> InResultType) throws -> OutResultType
) rethrows -> OutResultType
However, move
differs in a couple of important ways. For one, it does not operate on Swift values. It operates on bindings, and even more specifically on bindings to storage. These are not concepts that the type system exposes to users in any form today, and I am not aware of any plans to expose such concepts to the type system. The type semantics of move
are trivial (it's the identity function), but move
has implications at both the syntactic level ('is the argument written as a binding?') and a deeper semantic level ('does the binding refer to actual storage?').
Additionally, the move
'function' has effect on the semantics of the code exterior to the call of move
itself. AFAICT, this does not apply to withExtendedLifetime
or withoutActuallyEscaping
whose effects (and all with
-style functions) are explicitly constrained to the body of their closure argument.
IMO these are strong reasons to make move
a piece of syntax—its integration with the language is much deeper than is able to be expressed by a stdlib function or the Swift type system.
Suppose we were proposing the guard
functionality before it was around, and suggested an stdlib function with the following signature (and lifting the restriction variadic auto closures... ):
func guard(conditions: @autoclosure (() throws -> Bool)..., else: () throws -> Void) rethrows
But that doesn't give us all the power we want from guard
, so we additionally propose some 'extra semantics': each of the conditions
is permitted to be a pattern, bindings introduced by each of conditions
are introduced in the outer scope, and if the else
block is executed then guard will return control to the caller two levels up.
Sure, we could have approximated the language-construct version of guard
with a function, but it would have seemed very weird to me to pretend that it's 'just' a library function when its interaction with the language and the calling function is really much more fundamental. IMO it's the same for move
(though I admit move
as a function is a bit less ridiculous than my analogy ).
EDIT: to make my point a bit more directly (sorry for the rambling post), I think that expressing move
as a function actively interferes with the understanding of the feature. Under Swift's function call semantics, as I understand it, both the T
passed in to move
and the T
returned from move
should be (semantic) copies, but move
is very specifically not copying its argument/return value.
It's not look intuitive. I think it's better to use drop(x)
to end lifetime.
I like @tera’s point that move
takes something akin to an “in” parameter, which we don’t have in Swift today. Perhaps that’s an avenue worth exploring particularly as we think about other pieces of the ownership story.
A few things:
- The "in" convention already exists in the language as the default convention for inits and non-self setter args.
- Additionally, we already have an attribute override for this using the __owned attribute. That being said, that needs to go through evolution for us to be able to use it. I am hoping to find the time to write a proposal for that soon, but no promises! (It would have a different name though, consuming instead of __owned). NOTE: It is distinct from move itself since consume doesn't imply the special move data flow rule.
- move is using __owned, so it does have the "in" convention.
Can someone help me understand why you would ever want to write let y = move(x)
(i.e. assign to anything other than _
)? The author uses this construct in the pitch, and I'm not sure whether that is a mistake or intentional.
Consider the following Swift:
struct SortedArray {
var values: [String]
init(values: [String]) {
// Ensure that, if `values` is uniquely referenced, it remains so,
// by moving it into `self`
self.values = move(values)
// Ensure the values are actually sorted
self.values.sort()
}
}
can we have let b := a
suger for let b = move(a)
Why? Have any other languages used this syntax for move? Whereas several languages have used it for assignment.
let b <- a
would be more natural here. but then how best to drop?
_ <- a
nil <- a
unlet a
drop(a)
unbind(a)
remove(a)
_ = move(from: a)
...
Because I want move is more simply to use than copy, but making move default on COW type will be source breaking
Pitching and taking consuming
(née __owned
) through evolution before the move
function would probably clear up a lot of the confusion here.
When Swift can declare move-only values (on the caller side, or as part of the type), that's exactly how the move
or drop
function will work, without any magic, just like the Rust examples. The compiler Builtin
inside move
's implementation will be superfluous in that case. The nice thing about the move
that we're proposing is that it has the same semantics for any argument value. That does require magic to distinguish normal pass-by-value semantics from move semantics.
There are alternatives to using compiler magic to handle copyable argument values. I'm not a syntax afficionado, and I don't see any precedent in the language for something like this, but we should at least consider them:
- A
move
keyword that works as an operator
foo(move arg)
- A
move
parameter modifier
A free-standing move
function could be written as
func move<T>(_ t: move T) -> T {
return t
}
Or even expressed as a prefix operator.
Any user-defined function could force argument moves.
This runs counter to the current Swift practice in which parameter modifiers only affect semantics on the callee-side. Normally, without any recognizable syntax on the caller-side, argument semantics are unaffected. For example __owned
does not currently force a move (nor should it).
consume
is good, but it will be overloaded with other "consuming" uses, which consume their argument value, but do not prevent implicit copies and subsequent uses of the variable. move
consumes its value and also ends the variable lifetime.
Just because something can be spelled a certain way doesn’t mean it should be spelled that way. For example, functions can implement the exact same semantics of computed properties. But Swift chooses to give computed properties the same syntax as stored properties because it helps the programmer’s understanding.
I would argue that modeling move
as a function harms the programmer’s understanding. The classical definition of a function is a map from domain to range. In this light, move(_:)
is the identity function. It’s only when you expand the definition of function
to include its effects on the calling environment that you can even start to explain the semantics of move(_:)
.
As you illustrate, there’s a good chance that a keyword will be required at the call site to indicate that the client is unbinding the value. Why add the extra layer of function call syntax atop that? Let the keyword do the heavy lifting.
Would it be possible to somehow reference the target binding, rather than using the assignment operator? Assignment isn’t what’s happening, after all. I think that’s the crux of the issue here.
struct SortedArray {
var values: [String]
init(values: [String]) {
// Ensure that, if `values` is uniquely referenced, it remains so,
// by moving it into `self`
move(\values, to: \self.values) // Syntax of local key path expressions subject to change
// Ensure the values are actually sorted
self.values.sort()
}
}
That would still require a tweak to the grammar to allow local key path expression literals (which need not resolve to a public KeyPath
type for the moment), but I think it’d fit what is actually happening much better: you are operating on bindings, not values.
For that to work, you’d also need some way to make new declarations with let
or var
, ideally while preserving type inference, so obviously the idea needs some work.