[Pitch] SE-0377 Amendment: Making `borrowing` and `consuming` parameters require manual copying with a `copy` operator

Back in December, while SE-0377 was being reviewed, I had raised the possibility of making borrowing and consuming parameters not implicitly copyable. At the time, the Language Workgroup had accepted SE-0377 without making a final decision on this aspect, since we had planned for further proposals such as local inout and borrow bindings to make the case for this. Those other proposals still have implementations under development, but with the borrowing and consuming parameter modifiers otherwise ready to ship with the next version of Swift, we want to make sure they end up in developers' hands in the form we ultimately want them to have. As such, I'd like to formally pitch the following language changes:

  • A parameter binding marked with the borrowing or consuming modifier is not implicitly copyable within the function body, behaving like a value of noncopyable type.
  • If such a parameter is of copyable type, then it may be explicitly copied using a new copy x operator.

Here is the proposed amendment to SE-0377:

There are several reasons we believe we should make this change:

  • The borrowing and consuming modifiers are never strictly necessary to use with copyable types, so if a developer is adopting them, that is a strong indication that they are doing so because they are trying to optimize the ownership behavior of the function. Making copies explicit gives the developer a tool to validate that explicitly stating the convention actually eliminates copies in the way they expect it to.
  • As currently pitched, the borrow local variable binding is also proposed to not be implicitly copyable. Making it so that borrowing parameters behave like borrow bindings provides a consistent model for these related concepts.
  • We had previously pitched the idea of a @noImplicitCopy attribute, but the community rightly brought concerns about the verbosity and annotation burden of this approach. As an alternative, we'd like to reach a point where borrowing/consuming/mutating are the way to opt into working with the ownership model, and we can provide consistent behavior between copyable and noncopyable types within the ownership model by suppressing implicit copying for ownership-related bindings.

The no-implicit-copy behavior will only apply directly to the parameter binding itself. If another variable is assigned the value of the parameter, or the parameter value is passed along to another function, then implicit copies may be allowed in those other contexts:

func foo(x: borrowing String) {
  func dup(_ y: String) -> (String, String) {
    return (y, y) // It's OK for another function to copy values passed in
                  // from a no-implicit-copy parameter
  }
  let (x1, x2) = dup(x) // OK
}

func bar(x: consuming String) -> (String, String) {
  // This would be an error without an explicit `copy`
  // return (x, x)

  let y = x // But it's ok to implicitly copy `y`
  return (y, y)
}

I'm working on a proper proposal document, which I'll link here when I have ready, but I wanted to start getting community feedback about this change. Thank you for your feedback!

27 Likes

Does this also apply to a method annotated with a borrowing modifier, i.e. does self require manual copying in that context?

1 Like

Yeah, using borrowing or consuming as a method modifier would apply the no-implicit-copy constraint to the self binding inside of the method.

2 Likes

Big fan of this direction—this reasoning:

resonates strongly with me. I still have some discomfort with the same thing I raised when @noImplicitCopy was being discussed:

It seems a bit odd to simultaneously say "we think these modifiers indicate that the developer really cares about explicit ownership behavior," while also allowing the protections proposed here to be dropped on the floor in many cases.

I realize that writing dup(copy x) isn't great because the fact that dup ends up copying its argument is really an implementation detail and not something that's happening at the call site, and I also don't really like the idea of some sort of allowCopy x operator. I don't know that I can totally justify you can't pass this borrowing x to an implicitly borrowing parameter sigil because one was explicit about its ownership and one wasn't"—it seems like it could potentially have a very high annotation burden.

OTOH, at the same time I am wary about adopting somewhat fragile protection against implicit copies since users may think they're protected from implicit copies but then accidentally pass a borrowing parameter off to a function that is not so careful. And as noted:

so users who do feel like "wow, being explicit about all the ownership info is more trouble than it's worth for this parameter" are free to opt out wholesale.

4 Likes

I still feel like there’s a gap here around mutating/inout, but I don’t have any good ideas of what to do about it. That doesn’t have to block this change, which I otherwise like, but it does feel like a broken stair.

6 Likes

There are two ways to "drop it on the floor": assign it to another binding, or call another function. In the fullness of time, when we do have a full set of ownership binding kinds that also don't have implicit copyability, then an implementer has the option to continue using those binding kinds locally to maintain transitive control of where copies occur, and using let or var would be more of a conscious choice to let go of that control where it's no longer needed. In my mind, what other functions do with the value is generally out of the current function's control, and being able to deploy the ownership modifiers to locally control copying behavior on a function-by-function basis still seems valuable even if you call out to other functions that may copy.

2 Likes

This makes sense. I agree that in a world with all the different binding options let and var are probably good enough as the affirmative signal.

I think part of my worry is that this feels not super robust against local refactoring. If you pull out some logic into a separate function then you must remember to also explicitly copy over any of the ownership modifiers you care about, otherwise you may have accidentally gotten rid of the protections you had before, and can freely make copies in the new function without the compiler stopping you. And of course, the original addition of the ownership modifiers, the later refactoring, and the introduction of unintentional copies may happen at completely different times and be done by completely different people.

9 Likes

This is a natural, expected consequence of Swift's copy-by-default semantics. All programmers need to opt-in to ownership control whether they're writing new code or refactoring old code. This proposal doesn't limit future possible safeguards though, like no-implicit-copy concrete types and no-copy performance assertions for the truly paranoid.

No matter what else we do, we'll need parameter and variable bindings that signify ownership--and those better have sensible, consistent semantics for both copyable and non-copyable types at the outset. With this proposal, the only inconsistency we'll need to worry about later is our current mutating self semantics--assuming we want to reuse name mutating for ownership control. I hope we consider changing those semantics under the next language mode.

4 Likes

Copyable protocol please. I want to control the behaviour when I make a copy.

I'm with you there, but we're talking about a situation where ownership control has already been opted into and someone is making changes that defeat that control. It seems pretty easy to do that accidentally. IOW there's no discomfort with the fact that defining a new function requires you to opt in to ownership control, but rather with the fact that the value which has already opted in to the ownership control can be so easily forwarded to a parameter that doesn't.

Another thing on my mind in a similar vein is the information that can be gleaned from reading a function that takes a parameter with an ownership modifier. As-proposed, you get "copies are potentially important to look out for with this parameter, and any places marked with copy definitely copy it.” That strikes me as a distinctly less useful piece of information than “all places which potentially copy it are explicitly marked.”

We similarly require explicit marking of all potential throwing and suspending points with try and await. I get why potential copies are perhaps not as concerning, but we’ve already narrowed things to apply only to cases were the user has given an affirmative signal that they really do care about calling out ownership/copy behavior explicitly. Is the concern that these potential copy points would just be so numerous as to render any marking scheme far too noisy?

6 Likes

This doesn't help when you have a concrete type, like the Data provided to a delegate method from an HTTP request. I really want that to be consuming and to not copy it accidentally when working with it, because it might be pretty big.

2 Likes

Certainly +1 on requiring explicit copy for such parameter bindings, however I echo @Jumhyn 's concerns. If I'm writing a function with consuming or borrowing it's likely for important reason, and I want the language to force me to clearly specify when I'm opting out of those semantics. If some function f(_ x: T) implicitly copies I think that not forcing f(copy x) breaks clarity at point of use, and could easily be misconstrued as a respective consuming or borrowing operation itself. In more complex software this could be a missed optimization opportunity where the user desires optimization because they may actually want f to be consuming/borrowing but just happened to miss that.

I understand the original intent of the proposal was to include "Adding consuming or borrowing to a parameter in the language today does not otherwise affect source compatibility...The compiler will introduce implicit copies as needed to maintain the expected conventions." but there's no breakage of expectations in that case, since the assumption there is that the variables passed into the functions were implicit copy already since that's the default. In a function where a variable is non-implicit copy my expectation would be for it to remain as such until there's some explicit declaration otherwise. I'm opting out of the default in those cases and expect having to be more explicit.

2 Likes

The ownership model contains consume and consuming keywords. This proposal proposes to mark copy explicitly. So, there isn't an issue for "Data". Copyable protocol has no conflict with that.

The subtlety is that by default function parameters are borrowed, so at the call site f(x) is a borrowing operation. However, without borrowing specified in the signature of f explicitly, we drop the "strict ownership control" behavior and the parameter may be freely copied within f implicitly. That also means that what I said earlier really deserves a bit more nuance:

What you would really get from requiring explicit markings is "all places which definitely copy or do not opt in to strict ownership control for the passed value are marked."

Maybe requiring explicit marking when re-enabling implicit copy behavior would be reasonable only as an intra-module rule? I think it would be fairly reasonable to say "once you cross the module boundary you've given up control of what happens to that value, you have to trust the implementor on the other side." That would keep it from being entirely too onerous to use an explicit-copy binding with a library that perhaps hasn't audited itself for explicit ownership modifiers, or just doesn't want to take on the burden of being explicit about their ownership.

OTOH—isn't that exactly the sort of library a user who does opt-in to explicit ownership control would want to think twice before using? I'm kinda of two minds here.

The last thing I'll say is that I think I'm on board with this:

That is, I agree that some compiler enforced checks for the cases we know we should call out are likely better than none. I do have a mild concern that diagnosing some issues may induce people to think that they can rely on the compiler to catch all potential copies by marking their ownership explicitly only to be surprised later on that that's not necessarily true. But it feels like that's equally a worry with having potential copy marks become so numerous as to be useless, so erring on the side of less noise seems like a reasonable choice.

2 Likes

Since the behavior prevents only implicit copies, but doesn't rule out explicit copies, then it seems like no matter which policy we take with function calls, you wouldn't be able to look at a piece of code like:

func foo(x: borrowing String) {
  bar(x)
}

and know whether and how often x is copied, without looking at the definition of bar. If bar is a function under your control, and its performance is suspect, then you could then apply the ownership parameter modifiers to bar to continue the chain of mandatory explicit copying. And if it's not under your control, you can't change it anyway, and the best you can do is minimize copying within foo. So I don't think a transitivity constraint would give developers any stronger guarantees or better control than they can get without it, and it would make the feature harder to adopt, since you could only adopt the parameter modifiers in bottom-up situations where you can edit code from the leaves and never need to call out into code you don't control.

4 Likes

Yeah, I’m coming around on the idea that it’s worth making things easier to adopt incrementally. If someone cares so much about marking all parameters in a codebase with explicit ownership (or all parameters of a specific type, or in specific files or whatever) that seems to be accomplished easily enough via a linter rule.

I think this is really surprising and will lead to confusion and people thinking they have guarantees that they don’t. If copying is explicit, I would expect to have to write let y = copy x.

11 Likes

I definitely see this point but I find myself agreeing with Joe's reasoning that for the ownership-minded author, let y = x is quite a clear signal that you're copying x, and it will only become more so once we have other types of non-copying bindings.

I wonder if this would feel any better if let y = x and/or var y = x were the canonical ways to create a copy of a non-implicitly-copyable binding. That is, don't provide any way to produce a temporary copy of explicitly borrowing or consuming parameters—if you want to use them in a way that requires a copy, you must create a new implicitly copyable binding.

I would like to write let y = copy x as well.

That means something different, though, since let y = x would normally consume x in order to move it into y (and if x is a borrowing parameter, you would already have to write let y = copy x in order to make a copy that can be given to y). When we have the borrowing bindings, you would be able to use those instead of let or var as a way to bind to x, or part of x, without copying it and carrying the no-implicit-copy constraint forward.

4 Likes