[Pitch] Move Function + "Use After Move" Diagnostic

I am not a big fan of move and I would be all for finding a new name that makes sense if it weren’t already the term of art. I think this is a case of perfect is the enemy of good when it comes to naming. I don’t think move is a good name but it is the term of art which makes it good.

If we want a function to just deinitialize/deallocate memory then we can have and name those differently from move.

I think having similar naming to the UnsafePointer operations is what we would want though. They have move, deinitialize, and deallocate.

It might even make sense to namespace them like Unmanaged, but I don’t have a strong opinion on that aspect.

I floated this idea in the performance manifesto thread, and I still kind of like it. It's clear for newcomers to understand, and if you could assert that multiple values are no longer used in a single statement, it would be a one-line addition to the function (rather than wrapping every assignment in a call to the magic function move()).

I really believe we should be trying to think outside the box here. One of Swift's biggest strengths is that it is approachable. Let's try to devise a syntax which beginners can understand without having to read articles on temporary variables and object lifetimes. They may not immediately understand why it's there, but they should at least be able to understand what it does.

2 Likes

With asserts the expectation is they are no-op in release mode and they do not speed things up (they might slightly slow things down in debug mode). Your version of assert would be somewhat unusual in these regards.

I still think here compiler can understand that values parameter is no longer used and do this optimisation automatically without a need of opt-in. Maybe that's just not the best example?

1 Like

This is modelled like a static assert (#assert), in that it's a compile-time thing. But another name would be fine; it's just a strawman.

Right - the compiler can perform moves automatically when appropriate. The point of the move function is to prevent the programmer from accidentally using the variable beyond that point and preventing the value from being moved (the "use after move" diagnostic). It's not about getting better performance; it's about stating your intent so you get predictable performance.

I quite like the idea of using drop, but I feel it still needs to be a bit clearer about why it's there.

func frobnicate(foo: Foo, bar: Bar, baz: Baz) {
  var foo = move(foo)
  var bar = move(bar)
  var baz = move(baz)
}

// We do get in to naming issues if using drop to ensure moves.
// Which is unfortunate.

func frobnicate(foo: Foo, bar: Bar, baz: Baz) {
  var _foo = foo
  var _bar = bar
  var _baz = baz
  drop(foo, bar, baz) // ehh. It's not bad... but can we do better?
}

func frobnicate(foo: Foo, bar: Bar, baz: Baz) {
  var _foo = foo
  var _bar = bar
  var _baz = baz
  #assert(no longer used: foo, bar, baz)  // clearer, IMO. Not perfect.
}

stopUsing(x, y, z)

Or more precisely allowDeinitialization(for: x, y, z)

2 Likes
unlet x, y, z   // ?
2 Likes

I would be somewhat hesitant to use a new term given that we already have API using "move" for this meaning: Apple Developer Documentation

2 Likes

That's true. But then again, the unsafe pointer functions do not unbind variables. It is not precisely the same thing as the move function + use after move diagnostic.

1 Like

I would argue that they do unbind it, it's just that the language doesn't support that yet, so they do so unsafely (i.e. in a way that doesn't provide any diagnostics if you misuse it).

3 Likes

What I'm trying to get at is that perhaps we shouldn't be manually writing the individual operations and optimisations which the compiler will perform; we should describe the high-level thing (unbinding/preventing further use of a variable) which allows the compiler to perform moves.

With unsafe APIs, you are explicitly taking the wheel and performing those individual operations manually (e.g. Unmanaged allows manual retains and releases, words which otherwise do not appear in the language or stdlib - we use higher-level concepts such as weak and unowned instead).

So I don't think it's such a deal-breaker if we say that unbinding a variable ensures that its last assignment will be performed as a move. But of course, it depends how much we gain; if the feature becomes much more understandable, it may be worth some slightly overlapping terminology.

When I first learned about moves, I had to wade through countless articles describing the intricacies of the C++ standard and how temporary values are handled. It really hurt my perception of what was happening, and made it seem more complex than it really is. I hope we can avoid a situation where a Swift newbie sees a bunch of move(x) calls in a project they'd like to contribute to, searches for what moves are, and finds a bunch of jargon like I found.

2 Likes

Can't disagree that UnsafePointer and friends are a bit much, but I think that's a design goal. It's perhaps more concise than PointerWithNoGuardsBewareTheSegFaults. :)

I quite like unlet x how about unbind x and y = rebind x ?

2 Likes

I’m not opposed to using the term “move”; I’m opposed to using the term “move” in isolation. That example, ungrammatical though it may be, chooses to favor clarity over brevity. Parameter labels are great: if this is going to be expressed as a function, let’s use them.

1 Like
Tangent on Pointer Names and Current Naming Conventions

If someone wrote that API now, I’d probably ask why they aren’t breaking it into protocols.

protocol Pointer {}
protocol RawPointer: Pointer {}
protocol MutablePointer: Pointer {}
protocol BufferPointer: Pointer {}

Even marker protocols can be really useful for describing an interface.

I’d probably advocate for shoving the implementations in an uninhabited enumeration called UnsafePointer, too.

1 Like

Dear Swift,

Oh, this is an intriguing challenge. Here are my humble thoughts on the matter.

Let us task some certain figure with a job. The job is simple. It is to look after a value. As an aside, we note that the figure and the value it is looking after are clearly not the same thing.

For the sake of the argument, let our figure be x. As for its job, we will task it with looking after the meaning of life. In Swift, unless I am mistaken, we would give this figure its job in the following manner:


let x = 42

Splendid.

Now, we will imagine that this figure has been doing a fine job, but now the time has come for another figure to take up the task. Looking after the meaning of life is surely not easy. It seems inevitable that this task might need to be passed on to some other figure eventually.

At the same time, we also realise this: The meaning of life is quite a big deal. It is not something which can just, willy nilly, be copied about. It is substantial, and a rather tremendous thing.

So what we must do is quite clear. First, we need to prepare a new figure; for the sake of the argument, let this figure be y. Then, when y is ready, the moment will come. Upon that precise certain moment, x must retire. Furthermore, as x retires we must assign the task to y. From that moment on, y, and not x, must look after the meaning of life.

So now we know what to do, all that remains is to spell it all out. I suppose there are a few ways it might be done. So here are a few.

If we don't mind making Swift just a little bit bigger, we could say:


let y = retire x

If we don't mind peeking through the looking glass, we could say:


let y = retire(x)

For less drastic jobs, where no figure, perhaps, needs to take up the task, I agree that it seems quite natural for no figure to do it:


let _ = retire x

let _ = retire(x)

Or, quite simply:


retire x

If it turns out that the reason for the transition has nought to do with age, then perhaps x could resign instead:


let y = resign x

let y = resign(x)

In either case, whether it be to retire or resign, it is clear that x is giving up the job.

To provide a balanced alternative (should we prefer convention over analogy), we could simply 'add value' to the term of art in some manner which makes the outcome of the process as clear as possible (remembering that the outcome of this 'function' is somewhat unusual, and so a little extra guidance for the poor human would surely not go astray).


let y = moveValue(from: x)

In such manner, all those hardened cases who type 'move' in their sleep can still write 'move' to their heart's content. I suspect that code completion will then take up the slack, and pop in all those extra characters, voila. We note also, that we are now being much more specific (compared with simply saying 'move'). The likelihood of treading on someone else's toes is substantially reduced.

In any case, it makes one nervous. Once the decision is made, it is hard to go back. What a wonderful adventure it is!

With my best regards.

Edit: Please note that after writing up this post, I have realised that using the word retire has already been put forward by @Matt_McLaughlin (using a slightly different analogy). Needless to say, I think it's an idea worthy of serious consideration. Thanks Matt!

4 Likes

I’m sorry to beat a dead horse but I’ve read every post in this thread and I’m still very strongly confused and against the spelling move as a stand-alone top-level function.

If we’re adamant about using that specific term of art because of c++ and others, I still think trying to make this work as a keyword is worth a source-breaking change (would this not be auto-migratable?)

If we are stuck with a top-level function, I’d be for just about any of the other spellings that have arisen in the thread. The recent arguments that we should be modelling high level concepts in the language and not what the compiler itself is doing internally are very compelling to me. Yes, precedent is important, but Swift is not C++ and I for one am very glad of that.

2 Likes

In the pitch all more or less realistic examples of move are of the first two forms, not the third:

1. _ = move(x)
2. useX(move(other))
3. let y = move(x)

This prompts a question, do we need the third case? Where would I need to use it and will have to use y instead of x ?

Edit: I can think of a use case when variable starts as var, gets modified perhaps a few times and then matures and becomes let. Once matured it can not go back to var. In this case I'd probably want to use the same name for the variable, although if have to, can use a different name (via the pitch proposed move). Example:

var x = 1
foo(x)
x = 2
make x let somehow
foo(x)
x = 3 // error
// no way to go from `let` back to `var`

Though I think this is not very frequent use case. I hope I am missing some other more useful example for move.

In theory, you never need move(_:) at all. It’s a way to assert manual control over a detail that is normally automated by the compiler.

#1 and #3 are really the same form. I believe #3 is mainly useful for the sake of code clarity, as well as the ability to rebind a constant as a variable without potentially introducing a copy.

1 Like

To keep things moving, I've coalesced the previous drafts and other documents about the move function into a proposal PR:

17 Likes

Great proposal! I'm sooo excited seeing Ownership progressing!

I'm curious as how this proposal fits with roadmap of Ownership / the plan laid out in the Ownership Manifesto?

Fairly early in the Ownership Manifesto, three "tentpoles" are presented:

Swift already has an ownership system, but it's "under the covers": it's an implementation detail that programmers have little ability to influence. What we are proposing here is easy to summarize:

  • We should add a core rule to the ownership system, called the Law of Exclusivity, which requires the implementation to prevent variables from being simultaneously accessed in conflicting ways. (For example, being passed inout to two different functions.) This will not be an opt-in change, but we believe that most programs will not be adversely affected.
  • We should add features to give programmers more control over the ownership system, chiefly by allowing the propagation of "shared" values. This will be an opt-in change; it will largely consist of annotations and language features which programmers can simply not use.
  • We should add features to allow programmers to express types with unique ownership, which is to say, types that cannot be implicitly copied. This will be an opt-in feature intended for experienced programmers who desire this level of control; we do not intend for ordinary Swift programming to require working with such types.

These three tentpoles together have the effect of raising the ownership system from an implementation detail to a more visible aspect of the language. They are also somewhat inseparable, for reasons we'll explain, although of course they can be prioritized differently. For these reasons, we will talk about them as a cohesive feature called "ownership".

AFAICT move (and "use after move" diagnostics is neither of those tentpoles?

So I wonder, is the Ownership Manifesto outdated and needs an update? move is mentioned rather late in the Manifesto, maybe it is just how my brain works, but I would have expected that the features to be implemented first in the journey towards Ownership, would be mentioned first/early in the manifesto.

Can the Core Team give a general update to the roadmap of Ownership?

Few us like to give estimates on when a feature is shipped, but I'm going to be annoying and ask anyway, is there a plan for Ownership to be implemented during Swift 6.X (not 6.0 or 6.1..), or is it rather Swift 7.X? Or is this completely unknown.

Also, what is the prio of Ownership compared to any other major language feature? Does it have higher prio than let us say Variadic Generics? Or is Variadic Generics """easy""" ("""small""" amount of work) compared to the vast feature(set) of Ownership? (tripple quotes to mark that I do not believe Variadic Generics to be easy to implement, but rather Ownership seeming to be a beast in comparison).

Cheers!

4 Likes