Exactly.
I guess this move
function/keyword is intended as a last resort manual-override and not as the preferred way to specify ownership and lifecycle of values. I find it difficult to reason about the move
function when we are not sure where we will have to use it because the future high-level concepts are still vague.
I read the (revised) proposal, and I have been following discussions about this feature for months. Overall, I get it, but I donât really get it.
FWIW I definitely find this version more palatable than the âmagic functionâ variant from the first version of the proposal. It certainly feels more aligned with the feel and direction of swift.
That said, to me, calling a function with the owned
modifier on its argument would feel like a better way to specify that the lifetime of a variable should end at that point. Or a tightening of lexical scope as a way of dictating variable lifetimes.
If adding this move keyword is the preferred way of signifying that an otherwise copyable type should be moved instead of copied, i.e. that we always have to add this modifier to an owned function argument, unless the type itself is a speculative âmove-only typeâ, then thatâs the bit I kind of get. I see some analogue here with inout and the & modifier. If thatâs the point, then I guess I can live with that.
Overall though, I donât really see the value in this feature. I donât buy the argument that adding move
will signify to future maintainers of my code that the lifecycle should end and therefore they will not change the code without thinking twice. And I donât really understand the argument for this feature rather than more formally defining lexical scope which seems like a much more intuitive way of defining a variableâs lifetime. The proposed feature seems to just be complicating that intuition further, and encouraging long and meandering functions that do many things.
I am an avid Swift user (starting pre-1.0) and I work on and with high-performance low-level and algorithmic Swift code on a weekly, if not daily, basis. Predictable performance, including regarding providing hints to the compiler for ARC optimisations is an extremely valuable prospect to me. And yet I canât think of any cases where this proposal would benefit the work my colleagues or I am doing.
I donât know if this is an issue with the description of the problem being solved here, whether it somehow just doesnât affect me, or if Iâm still just ânot getting itâ, but overall Iâm still not in favour of adding this to the language. Maybe this proposal is just not the best place to start - maybe this only really makes sense in tandem with a few other features Iâm not yet familiar with. But from my current perspective itâs a -1 from me.
edit: I kind of feel like this feature will be introduced anyway. In which case I am also more for the take
spelling
ââ
Another edit: after sleeping on this I realised Iâd feel far more comfortable with this as a language feature if it was only allowed when calling a function (doSomethingConsuming(myParam: take y)
), as a parallel to useInout(myParam: &y)
.
There are numerous examples in the proposals along the lines of let x = move y
that make little sense to me - whatâs the advantage in ending one lifetime and simultaneously beginning an equivalent one? Is this really something we expect people to do?
In short: I would be +1 on a proposal that proposed only a way to spell out the fact that a function is consuming its argument: without the move
re-binding, which seems uncompelling to me, and without the drop x
semantics, which IMHO complicates intuition about the lifetime of variables within a given scope for unclear benefit.
But, since we donât having âconsumingâ functions yet, this proposal still seems premature to me without others being made in tandem.
Can move
also extend the lifetime of a binding rather than limit it? I'd assume that if there's a _ = move y
at some point in a function, y
would be strongly referenced until it gets to that point, but there's also an alternate interpretation where an "unnecessarily" far-out move-out is preempted by the optimizer and the object is still released early.
Currently, you can extend the lifetime of an object using withExtendedLifetime
, but it feels clumsy to me.
This is the kind of thing that leads to us being unable to evolve the language because proposals are either unreviewably large or contain questions that canât be fully resolved until later.
If other features in the roadmap will introduce keywords that should match or contrast this one, the authors should do their best to select all of those keywords now, even if this proposal only adds one of them. They can even describe how the keyword in this proposal is intended to fit into the bigger picture. Even in the worst case where they later decide they made a bad choice, they can still change this featureâs keyword in a future proposalâpossibly without even giving the old one a deprecation cycle if theyâre quick enough.
I agree completely. This feature is clearly one part of the larger ownership feature. We should absolutely select these keywords together with the knowledge of where we're going. At the same time, we do have to review individual features separately, or else we'll only get very general feedback rather than useful and in-depth consideration of the specific implications of each feature (such as people strongly preferring a keyword operator over something that reads like a function call).
There have been multiple threads about ownership as a whole, and this specific feature was extensively pitched; still, there's nothing like a proposal review to actually force people to start paying attention and express their opinion. This is how we flesh out more of the borrowing story, or at least the superficial aspects of it. If we need to drag out this review a bit or even go back to pitch, so be it, but the solution to this evolution problem cannot be to craft an omnibus design that will be reviewed all at once.
Naming is hard but I do agree that it should not be a blocker. Happy to see this as the plan of record to make forward progress in the borrowing story.
i disagree with this. there is one major use case that cannot be expressed with lexical scope: temporarily releasing self
in a mutating enum method. for me at least, _move(_:)
has allowed me to get rid of many case _modifying
or self = .none
hacks i used to rely on.
Do you have an example of this? I can't think of how it would be useful to release a value.
First, I want to say I'm very excited about the prospect of ownership-related features in general. Introducing a fundamentally new concept into a well-established language, in public, must be an immense task, and I appreciate the amount of thought and care it surely takes to navigate this.
This is where I'm struggling, personally. I am very much in this camp, but only in the absence of owned
as an argument modifier. Without the argument modifier it's difficult for me to pin down what move
is other than "compiler magic", which feels very much like a keyword*; but with the modifier it's clearly "just" an identity function that takes its argument as owned
âsomething that falls naturally out of more fundamental concepts in the language. In a world with the argument modifiers move
as a keyword is just syntax noise.
*I know there are "compiler magic" functions/intrinsics already (like unsafeBitCast
), but those pass the duck test for "is a function" in a way that move()
doesn't to my mind
owned
and shared
are discussed as future directions, but future directions aren't promises. I can meta-game and infer that these keywords are probably coming, but I think it's generally agreed that proposals should be evaluated on their own merits. Given the proposal on its own I have one piece of feedback, but in a hypotheticalâbut likelyâfuture I have the opposite feedback.
In this case the difference is ultimately pretty minor, but I think it highlights some sort of impedance mismatch in communication. From my perspective as a random community member there's no roadmap; it's clear that this is related to ownership but what that means in specific terms is unknown. Should I expect this feature to land in a release on its own? Or should I expect more proposals that flesh out a bigger picture to target the same release? Does this go before 6.0, in which case we have a "source breaks are OK" escape hatch, or does it live indefinitely? It wasn't obvious to me before that @beccadax 's suggestion of quickly revising without a deprecation cycle is even an option. This is in contrast to core team members and others with more inside knowledge who surely have at least rough schedules and task orderings in mind.
Again, the social dynamics alone of pitching and landing such a large project are clearly gnarlyâI very much appreciate, and do not envy, the core team in this regard. And I understand there are constraints that make more explicit communication difficult or undesirable. Clearly there's not an easy win-win answer hereâbut as a point of reference I feel like I remember much of async
as being a set of proposals posted in quick succession that inter-referenced each other. This obviously has its own set of challenges and tradeoffs, but it did make it clear that they were a family of sub-features that were, generally, expected to ship in the same release. (I'll also admit that for various reasons I wasn't actually following the forums closely at that point in time, so I may be off base here)
Like so?
enum Foo {
case bar([Int])
mutating func append(_ element: Int) {
switch self {
case .bar(var array):
_ = move self
array.append(element)
self = .bar(array)
}
}
}
I always assumed/hoped that the optimizer would do this automatically. I guess it doesnât?
The optimiser is not always able to do this, particularly if it cannot prove that the old value of the enum is not otherwise readable. In your example, the optimiser absolute can do this: the law of exclusivity means no-one else can read self
, so no-one can read the array. But if you are switching over an enum you're storing in a var
, instead of over self
, then this gets a lot harder. Being able to explicitly say "I am making this enum dead in this scope" helps.
I should clarify that I know that there is ongoing work to continue to reduce the number of places where these workarounds are necessary, and I'm confident that work will continue, so ideally move
will not be needed in these places. But it's very useful in the meant time. For example, the following sample produces a copy-on-write, which move
could help elide:
func transform() {
let buffer = ByteBuffer(repeating: 0, count: 16)
var view = buffer.readableBytesView // This should consume `ByteBuffer`, it "wraps" it.
view[1] = 1 // This will CoW, because `let buffer` is still alive
precondition(view[0] == 0)
}
If we could implement buffer.readableBytesView
with move
and consuming
then this CoW would go away.
Even with a perfect optimizer, it's still possible for subtle changes elsewhere in the code, particularly capturing a variable in a closure, to lose the ability to statically analyze lifetimes. So move
can still act as a hedge to diagnose when those cases happen.
Is that what __owned
does? Is the proposed move
a direct replacement for __owned
?
No. I talked about this upthread: SE-0366: Move Function + "Use After Move" Diagnostic - #63 by beccadax
This would make a much more compelling example in the proposal, but it also immediately brings up the question of how it would impact API design. Having readableBytesView
consume/move buffer
seems difficult to explain or find a good spelling for. If we had move
-as-argument-modifier, then ReadableBytesView.init(_ buffer: move ByteBuffer)
would be a lot more straightforward:
func transform() {
let buffer = ByteBuffer(repeating: 0, count: 16)
var view = ReadableBytesView(&buffer)
view[1] = 1
precondition(view[0] == 0)
}
But then we still have the question of how do we get buffer
back out of the ReadableBytesView while simultaneously ending its lifetime. Which is the same problem we started with. (It also precludes the ability to use opaque types.)
EDIT: spitballing ideas for how to spell self-moving getters/methods:
struct S {
public mutating move(self) func wrap<W: Wrapper>(in: W.Type = W.self) -> W<Self> {
// `move(self)` methods must be `mutating`
// possible analyses the compiler could apply to `move(self) methods:
// - they must in fact `move self`
// - the return type must involve `Self`
// - flow analysis must prove that the moved `self` was passed to a method or initializer of the returned value (recursively applying this rule to account for intermediate moves)
return W(move self)
}
var wrapper: some Wrapper<Self> {
move(self) get { ConcreteWrapper(move self) }
}
}
Is it fair to say that move
-as-argument-modifier would imply __owned
?
Moving self
The proposal doesnât go into much detail on what happens if you move self
. The reader can make reasonable assumptions, but it would be good to have them spelled out:
struct S {
var prop = "Hello"
mutating func f() {
let x = move self
prop = "Goodbye" // presumably this is an error because `self` is unbound
self = move x // is this legal?
prop = "Hello again" // what about this?
}
}
there is one major use case that cannot be expressed with lexical scope: temporarily releasing
self
in a mutating enum method. for me at least,_move(_:)
has allowed me to get rid of manycase _modifying
orself = .none
hacks i used to rely on.
Thanks for the use case, I was looking for something like this.
Like so?
enum Foo { case bar([Int]) mutating func append(_ element: Int) { switch self { case .bar(var array): _ = move self array.append(element) self = .bar(array) } } }
So that's just to have this:
....
_ = move self
....
instead of that:
....
self = .bar([])
....
right? Don't see a huge win then.
Perhaps a much bigger quality of life improvement in this example would be with this alternative:
enum Foo {
case somethingElse
case bar(x: [Int], y: Double)
mutating func append(_ element: Int) {
bar!.x.append(element) // or
bar?.x.append(element)
}
}
So that's just to have this:
.... _ = move self ....
instead of that:
.... self = .bar([]) ....
right? Don't see a huge win then.
The win is in not having to construct the associated value, which cannot be optimized out if it has side effects or is defined in a resilient module.