SE-0366: Move Function + "Use After Move" Diagnostic

Yes, but if type conversions were the preferred way of spelling this API design pattern, we’d use UTF16View.init(_: String) instead of String.utf16.

Using type conversions for everything is a C++ism.

That string views are vended as properties to me reflects the expectation that users will or should predominantly operate on these views ephemerally—e.g.: str.utf16.forEach { print($0) }. Indeed, it’s worth noting that the API in question is spelled utf16 (not utf16View, as you wrote in your initial draft) to optimize readability at the call site for that use case: the emphasis isn’t on the instance of collection type that’s returned, but rather the elements of that collection on which you’re immediately going to chain some operation. For that purpose, we can yield from _modify to avoid unnecessary copying.

If, instead, we expect users predominantly to store bindings to these string views when using them, then it would be worth a discussion as to whether the spelling should be as a type conversion. Now, I have not used ByteBuffer[View] (although, now that I think about it, I can think of a great place to use it the next time I rewrite something) and can’t say whether that’s the case for this particular API. However, the example you put forward is premised on that use case, since it’s about undesirable copying that happens when making use of the getter instead of _modify. My point is that spelling such APIs as conversion initializers is aligned with Swift API guidelines, and prioritizing a way to avoid COW for that spelling is not some “regression” in expressiveness of the language.

This is not to say that I am against the impulse to improve getters also to avoid unnecessary copying, but it is not necessary to label the existing API design guidelines as regressive or a “C++-ism” in order to make that point.

1 Like

This is a mischaracterization of my point. I did not say that the existing API design guidelines are regressive. I said that it’s a C++ism to design an API such that the client obtains wrappers by constructing a concrete type around an instance, and that it would be a regression in expressivity if this were to be the exclusive way of designing such an API.

I then gave an example of a highly visible wrapper in the standard library that I believe is exemplary of the standard library’s (and language’s) design sentiment: clients obtain wrappers by calling property getters, rather than by constructing wrapper types around instances.

It’s arguable this discussion is beyond the scope of the proposal, but I think it has become apparent that it’s difficult to analyze the move statement/expression in isolation from the broader goal.

2 Likes

isn’t yield just sugar for

mutating 
func modify(_ index:Index, _ yield:(inout T) throws -> U) rethrows -> U 
{
    try yield(&self[index])
}

?
at least that’s how i always understood it…

String’s .utf16 does call an init on UTF16View, so it is using initialization for type conversion.

Yes, but that’s not how the client spells it.

I think perhaps I missed your point, then.

My point is that there’s a difference between this developer experience:

    myString.utf16.count

and this one:

    UTF16View(myString).count

… even though expression 1 boils down to the equivalent of expression 2 at runtime.

Swift currently allows the designer of String (and of UTF16View) to decide that clients who need access to the number of UTF-16 codepoints in a string get them via the first formulation.

@lukasa said:

…and my response is that this doesn’t entirely describe the problem. ReadableBytesView is the SwiftNIO analogue of UTF16View. Part of designing move semantics is figuring out how to give SwiftNIO the option of vending a ReadableBytesView via the existing getter syntax while avoiding the COW.

This will require some form of annotation on the property getter, and since I suspect there is a design for move semantics that is expressed entirely by annotations on function arguments and return values, I am trying to play that out in this thread before the language workgroup commits to a move expression.

4 Likes

As others noted, the main difference is that borrow makes explicit a shared borrow (which is for most normal types an immutable borrow), whereas inout indicates an exclusive borrow (a mutable borrow). I like the idea of keeping & to mean "this call may mutate this argument", and I'd also like borrow/take parameters to be usable as optimization tweaks without obligating a client-side code change.

4 Likes

Thanks. I can appreciate the logic in keeping & as a sigil for a mutable borrow. As the proposal mentions, and as the long digression above concludes, properties can’t be moved from without additional self-consuming semantics. But they can be borrowed from, which would make the applicability of & potentially confusing if it were also required to pass a move parameter.

There's a precedent of && for a similar thing in another labguage we can ... steal borrow :rofl:

1 Like

Aesthetics aside, there will be a need for a syntax to denote explicit copying, and there’s a limit to the number of & variants people will want to remember.

Unlike move and borrow, which place extra constraints on the typical compiler behavior, copy's behavior is captured pretty much by just a regular function:

func copy<T>(_ x: borrow T) -> T {
  return x
}
3 Likes

This touches on something I’m not quite sure about. You mentioned wanting to allow take (or move) to be be adopted by the callee without requiring a code change. It’s still an ABI change, though, right? If the argument is being populated from a variable, the caller has to move it out.

What’s a practical situation when a callee might do this? Other than as an implementation detail of move-as-function?

In typical callers, the compiler is free to copy or not as much as it needs to, so there's generally no noticeable difference, besides the net number of retains and releases, between calling a function with a borrowed parameter vs. a taken parameter. If you have:

class Foo {}
class Bar { var foo: Foo }

var global: Foo?

func callee(_ foo: Foo) {
  global = foo
}

func caller(_ bar: Bar) {
  callee(bar.foo)
}

then with the usual default receive-by-borrow convention, we'd end up with retain-release calls like this:

func callee(_ foo: Foo) {
  // Retain `foo` so that we can stash a copy of it in global
  _retain(foo)
  // (assignment dance elided for readability)
  global = foo
}

func caller(_ bar: Bar) {
  // Retain `bar.foo` around the call, in case another reference to `bar`
  // might mutate it during the call
  let foo = bar.foo
  _retain(foo)
  callee(foo)
  _release(foo)
}

Since callee always consumes its foo parameter by assigning it into global, it's an optimization to make it take the parameter, so that it doesn't need to retain inside the callee, and the caller doesn't have to release it after the call:

func callee(_ foo: take Foo) {
  // Don't need to retain `foo` since we're taking it
  global = foo
}

func caller(_ bar: Bar) {
  // Caller needs to retain a copy of foo for `callee` to consume
  // but doesn't need to release it after
  let foo = bar.foo
  _retain(foo)
  callee(foo)
}

so it is useful to be able to adjust the calling convention without requiring explicit callee- or caller-side annotations of the uses of the parameter or argument.

So are we effectively talking about a formalization of __owned, in which the caller implicitly generates a copy if necessary? Or are we talking about a new behavior where the value is moved out of the argument and the caller must explicitly copy the value?

As I said upthread, the latter definition is useful for spelling initializers of move-only types. But I get the sense you’re talking about the former?

In my mind, these are ultimately the same thing. For a move-only type, the parameter passing behavior at the call site isn't different. The lack of copyability ensures that the argument is borrowed or consumed directly. Explicit borrows or moves could still be useful as documentation or emphasis when working with move-only types, but wouldn't be necessary like they are for locally altering the behavior of normally copyable types.

Hm. The big difference in my mind is whether this code compiles:

func callee(_ arg: take Bar) {
  // ...
}

let bar = Bar()
callee(bar)
print(bar) // does this line compile?

If take is just a new spelling for __owned, this code compiles. If take behaves the way that I proposed move should behave, it doesn’t compile, because the lifetime of bar ended when the value was moved out of it and into arg.

Bringing this back to the proposal that we are ostensibly reviewing :smile:, if the latter is at all useful to have in the language, then I reiterate my stance that the move operator should be replaced with func move<T>(_: move T) -> T. And I think your example does a great job explaining how func copy() can be defined similarly.

If there isn’t a spot for something like move parameters, then I don’t have an argument against the move operator.

Either way, I believe assigning to _ should be required to drop a binding.

2 Likes

Agreed. Having the move operator be implicitly discardable seems a little too magical for me.

1 Like

I tracked the revision history of the proposal and was not convinced by changing move from function-like into such an operator. I would refer to this line in https://github.com/apple/swift-evolution/blob/main/commonly_proposed.md :

not somePredicate() visually binds too loosely compared to !somePredicate() .

Similarly, move x visually binds too loosely compared to move(x), which could make confusion for its users. This is also not aligned with custom consuming functions, making move too specialized.

Personally I’m not so satisfied with the name move, but it seems somehow a term of art and there’s no better alternative. Would be happy if we can take a prefix operator here, which can deprecate the naming and make the binding clearer.