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

If the same name is used by both the proposed global function and other methods (in the standard library or elsewhere) which API should have the @warn_unqualified_access attribute?

For example:

Sure, but IMO that generalization is only desirable insofar as the move function actually behaves like a function. At it stands, there are, in my view, important differences between move(_:) and other standard library functions, even the other standard library functions that are somehow 'special'.

I take @David_Smith's point that move feels ~function-y~ in some sense, but even with the example pseudo-implementation for move:

there would not be a way to express the fact that move operates only on local bindings to storage, rather than arbitrary mutable values (also, inout would prevent its use on let declarations).

Another thing the ownership manifesto mentions is that move would be unable to be applied indirectly, which I assume means that something like:

let f: (Int) -> Int = move

would be illegal? Or would the above work, but permit the following?

func doSomething(with x: Int, fn: (Int) -> Int) {
  fn(x)
  print(x) // has 'x' been moved?
}

doSomething(0, move)

I get the desire to avoid introducing keywords to the language willy-nilly, but if we have to introduce so many special cases just to call this a function, IMO we have a distinction in name only. Users would not be able to understand move just by reading the signature or implementation, so I expect it would inevitably have to be taught as "don't think of this as a normal function, think of it like this..."

Anyway I really want to make sure we don't dismiss the keyword option over minor syntactic issues that we can find ways to work around.

ETA: Also, if generalizability for the sake of future ownership features is part of the motivation for making move a function, the proposal should explicitly call that out. The only other such feature I'm aware of is copy, and although it hasn't been proposed, off the top of my head I can't think of why copy couldn't just be a function—AFAICT it obeys normal Swift function semantics.

9 Likes

When teaching iOS at the University, working with co-workers, contractors, or interviewing with Google. They have all found swift syntax and naming weird. ¯_(ツ)_/¯
For me I am totally used to the naming conventions and they make sense. It seems like it is one of those things some devs have to get used to.

The examples I remember most off the top of my head are the Unsafe[Mutable][Raw][Buffer]Pointer types and associated global functions. Naming of CodingKey/CodingKeys. Some other just odd things that don’t quite live up to the rest of stuff.

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   // ?
1 Like

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.

1 Like

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.