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

Clarity at the point of use, same as MemoryLayout. Think of it from the point of view of someone encountering it for the first time.

Huh? The function print accepts any number of arguments of any type. Hard to see how one can simultaneously argue that a function is both "exceptional" and "used often," but more to the point: it's both rather unexceptional given the plethora of top-level functions and, in no small number of settings (in performance-sensitive libraries such as the standard library itself), move is going to be used a heck of a lot more than print.

Namespacing is rather orthogonal to clarity at the point of use. Particularly if you suggest something like Binding that will require every SwiftUI user to specify Swift.Binding as opposed to SwiftUI.Binding.

Having been around and involved with discussions about MemoryLayout (I am also the author and implementor of SE-0136: Memory layout of values), we recognized that size, stride, etc. were properties of every type, but we didn't want to have static members cluttering up the autocomplete for every type. MemoryLayout<T> was a useful way to make them static members of a type parameterized on T without having that problem. By contrast, functions like type(of:) have no business being members, and we weren't shy at all about not namespacing them.

5 Likes

I meant that creating such a function is exceptional.

I don’t think that’s the intention. This is meant to be used only on occasion, not reflexively like in C++.

That’s an odd choice of example. I could certainly see that being a member instead.

I’d be a lot happier with a function named Swift.moveBinding(of:), if you’re dead-set against namespacing.

The proposed function doesn't move the binding.

  • if you are someone who expects to see __owned made into a general language feature at some point, then any argument sigil (such as &) that is chosen for move will also have to become the argument sigil we use to pass anything into any function that has the same ownership semantics as move.
func moveInitializeCudaArray(at pointer:UnsafeMutableRawPointer, 
    from x:owned [Int]) -> [Int]

let x:[Int] = [1, 2, 3]
let y:[Int] = move(&x)
// headers and backing storage of `z` now live at `pointer`
let z:[Int] = moveInitializeCudaArray(at: pointer, from: &y)
  • if you are someone who expects to see __owned made into a general language feature at some point, a language keyword such as del or unlet will immediately become redundant when that happens. removing them from the language will likely have to fight its way through the typical litigation of swift evolution, and will probably require some type of staging plan to move people off of the keyword.

  • we already have UnsafeMutablePointer.move. we do not have UnsafeMutablePointer.consume.

  • consume(_:) is not a good name for move(_:), because move(_:) returns its parameter to the caller. it would make sense to have a consume(_:)-like variant of move(_:), which returns Void, but it should probably be called deinitialize(_:) to be consistent with the rest of the language.

let x:[Int] = [1, 2, 3]
let _:Void  = deinitialize(x)
  • move(_:) is a common function name, and most people will adapt to this new reality by spelling it Swift.move(x).

  • present- vs past-tense for mutating vs returning methods only makes sense for instance methods. move(_:) is not an instance method.

  • in the following example, foo(s:) is to removeAll what bar(s:) is to move(_:). neither of these compile.

struct S 
{
    var x:[Int] { [1, 2, 3] }
}

func foo(s:inout S) 
{
    s.x.removeAll()
}
func bar(s:S) 
{
    move(s.x)
}

move seems very special and much more than just a small new funtion, so I'm quite happy that in the meantime, A roadmap for improving Swift performance predictability: ARC improvements and ownership control popped up.
Maybe that other thread will shed some light on the topic, but until than, I want to suggest a variation that afaics has not been mentioned yet:
As move is not even a regular function at all, but rather a hint to trigger some compiler feature, why not use the "compiler magic prefix" and call it like self.x = #move(&parameter)?

2 Likes

I'm not an expert in this field, but I understood the proposal, especially after reading through The Roadmap which makes a good case for introducing this.

My only question is why is move (and future copy) being introduced as a function rather than a keyword like return? It obviously does something that regular functions can't do and reminds me a lot of the discussion on any P versus Any<P> where the keyword was chosen.

2 Likes

That is somehow deeply painful to think of as Swift code.

Honestly, I’ve been waffling back and forth on whether to advocate for making this a keyword. I’m currently leaning towards keywords again, especially if it isn’t going to be put in a namespace. The risk of mistaking it for a normal function is too high.

Swift has been commonly criticized for having too many keywords, but I’m not entirely sure it’s a problem. If something doesn’t play by normal rules, having it stick out is a good thing.

Many people mistake that for boilerplate, but when I read Swift code I never see anything extraneous: every part always conveys information to either the compiler or the reader.

3 Likes

FYI, I posted a pitch for consuming/nonconsuming here: [Pitch] Formally defining consuming and nonconsuming argument type modifiers.

2 Likes

I've come around to supporting the functionality of this after reading Joe Groff's excellent roadmap, but am not a fan of the wording. move to me suggests something else, or a number of something elses. It's not really what's going on here. Here the object is being deleted from memory, so it can't be accessed afterwards. Whether something else retains a copy is secondary.

And using _ = move() is confusing. _ = XX. suggests XX is being evaluated, i.e. it's being moved, but then the result thrown away. But that's not really what's happening. And the English language can be used in a way already used elsewhere in Swift to avoid this.

E.g.

// to dispose of x call release
release(x)
// but if a copy is needed call released
y = released(x)

This mirrors the use of e.g. sort and sorted for Array. released is just a nice way of writing out both the copy and release, i.e. is identical to

{
    y = x
    release(x)
}

Doing it this way puts the release first as I think it should be. "release" has other meanings in computing but in code normally means something like this. Other possibilities include free & freed or dispose & disposed. I actually thought of expire and expired first but that's probably too odd.

3 Likes

i think the main reason for introducing move() is to move binding, for e.g.:

let y = move(x)

move() can be used for ending lifetime of binding, for e.g.:

let _ = move(x)

but i doubt "to end lifetime of binding" is the main reason for introducing move() function.

2 Likes

But that is what's happening. For _ = move(x) the value is being moved out of x then thrown away. The release is a side effect.

3 Likes

I keep thinking about the name of this and the fact that is just a function that can't be implemented in the language.

In my mind it would be perfect if the language could implement it int he same way that is implemented in Rust.

And after reading the roadmap it feels like it can get close but not completely....

So the purist in me feels that it shouldn't be a function. But then again, we see to have other functions in the language that are not implementable at user space so maybe a more pragmatic viewpoint is needed.

I think the problem is deeper: It's hardly a normal function at all...
Implementing the function-part is trivial (it returns the value you put in, so it is just identity), but the side effect is extremely strange (when comparing with ordinary Swift):
It's not only able to somehow change a value declared with let, it even "destroys" its argument, so that it can't be used anymore at all.
Given the fact that Swift is quite restrictive when it comes to redeclaring something without creating a new scope, this is a massive change (for what a function can do).

2 Likes

Moved out of and thrown away, yes, not moved. move on its own suggests something quite different. It could be called moveOutOfAndThrowAway but that's not very Swifty. But there are single English words which better describe what's happening, such as those I suggested.

It seems to me that one part of the syntax-level dilemma is that there's two ways of looking at move():

It destroys a binding, yielding the value previously referred to by that binding, which is not a concept expressible in Swift, and therefore feels "keyword-y"

or

It sets the value referred to by a binding to be uninitialized and returns the old value, which is mostly a concept that already makes sense and even mostly already exists (since you can have a variable declared with no initial value, and it'll error if used), and therefore feels a bit more "function-y".

Like you can almost imagine implementing move() like this:

func move<T>(_ x: inout T) -> T {
  defer { 
    x = uninitialized //doesn't actually exist
  }
  return x
}

I'm almost tempted to suggest = uninitialized as a spelling for this in fact, except that

foo(x)
x = uninitialized

is clunkier than

foo(move x)

(and might have ordering issues in more complex uses, since it's not an expression)

7 Likes

Indeed. And if we want to go even further, the next stage of "variable lifetime" could be "undeclared again" to return situation back to the state where you write "var x: T" (potentially with another type) again. In the scoped version this is equivalent to exiting the inner scope.

let x: A
foo(x) // error: uninitialized
x = expression
foo(x) // ok
uninitialize x // pseudo syntax
foo(x) // error: uninitialized
let x: B // error: already defined
unlet x // pseudo syntax
let x: B // ok

Sorry to ask this again: are there cases that can be elegantly expressed by move/drop machinery but can't be expressed by do {} scopes (or are too cumbersome in the latter form)? In other words is move a mere syntax sugar? Would like to see some killer example (with, say, 5 lines with new move machinery and 30 equivalent lines with old do {} scopes method).

let moving:[Int] = [1, 2, 3]

                  (moving)
                   (moving)
                    (moving)
                     (moving)
let moved:[Int] = move(moving)

If you’ve ever written any code that uses locks, you’ve probably encountered situations where cleanup happens on several paths, but at different times or in different orders. That’s a nightmare to do solely with scopes. Rust has shown how similar locking and memory safety actually are, so I suspect there would be similar caes for move().

4 Likes

Apologies if this idea has been discussed and dismissed elsewhere but I agree with the sentiment that move is a very unusual function compared to any other, which makes me think that it should be a keyword and not a function. Why should move be a function and consuming be a keyword? I know the latter kind of has to be but this seems inconsistent to me. I understand the motivation to make this a function to work with a cute usage like “_ = move(x)”, but do we really want our code to contain lines that look like that? A beginner would likely have absolutely no idea what that code would do, even if consumeX(move x) is fairly innocuous.

In addition, as an expert feature it seems too easily accessible IMHO, which makes me think it’d be too easy to get it wrong. Easy things should be easy, expert things should have some kind of warning sign attached, which IMHO a keyword would provide.

One other thing comes to mind, how does this interact with inner scopes: (edit: the detailed design suggests that the following would not compile after all - to be honest I’d understand why but it does mess with my understanding of the feature and / or scoping somewhat)

func foo() {
    let x = MyType()
    do {
        _ = move(x)
    }
    print(x) // possible?
}
1 Like