Selective control of implicit copying behavior: `take`, `borrow`, and `copy` operators, `@noImplicitCopy`

During the first review of SE-0366, we got a lot of feedback asking for more context about where it fits in our broader plans for ownership and performance control. I recently began a pitch for ownership modifiers on function parameters, which fill out our ability to control ownership across function boundaries, and I'd like to re-pitch the take/move operator as part of a collection of mechanisms for controlling ownership and copying within a function body:

  • the take operator, which explicitly ends the lifetime of a parameter or local variable (which was reviewed as move in SE-0366)
  • the borrow operator, which explicitly passes a property or variable by borrow without copying
  • an attribute for suppressing all implicit copying when working with a parameter or local variable, which I'll call @noImplicitCopy to begin with

The take and borrow operators provide tools for refining Swift's implicit behavior in common isolated situations, without changing the overall programming model in surrounding code. @noImplicitCopy, by contrast, fully suppresses implicit copying and requires working with affected values like a move-only type, maximizing control but requiring a more drastic shift in the programming model.

Note that we're not proposing these features to be a replacement for move-only types or for other systems programming features, nor will we stop improving optimization and solidifying the language model to make stronger guarantees about copying behavior. Even if future implementation improvements make these explicit controls less necessary to get optimal behavior, the annotations will still serve a useful purpose as a tool for performance sensitive developers to check their work, and for readers and maintainers of code to understand performance sensitivities in code that they should preserve. We also want to ensure that these features can be deployed to fine-tune sensitive parts of a codebase without virally reshaping the programming model of surrounding Swift code, allowing for other developers to continue being productive even if they aren't familiar with the fine details of ARC and implicit copying.

take operator

Proposal: SE-0366

it is useful to have an operator that explicitly ends the lifetime of a variable before the end of its scope. This allows the compiler to reliably destroy the value of the variable or transfer ownership of the value at the point of its last use, without depending on optimization and vague ARC optimizer rules. Consider this example:

func test() {
  var x: [Int] = getArray()
  
  // x is appended to. After this point, we know that x is unique.
  // We want to preserve that property.
  x.append(5)
  
  // We create a new variable y so we can write an algorithm
  // where we may change the value of y (causing a COW copy
  // of the buffer shared with x).
  var y = x
  longAlgorithmUsing(&y)
  consumeFinalY(y)

  // We no longer use y after this point. Ideally, x would
  // be guaranteed unique so we know we can append again
  // without copying.
  x.append(7)
}

In the example above, y's formal lifetime extends to the end of scope. When we go back to using x, although the compiler may optimize the actual lifetime of y to release it after its last use, there isn't a strong guarantee that it will. Even if the optimizer does what we want, programmers modifying this code in the future may introduce new references to y that inadvertently extend its lifetime and break our attempt to keep x unique. There isn't any indication in the source code that that the end of y's use is important to the performance characteristics of the code. We can introduce an operator to make that explicit:

func test() {
  var x: [Int] = getArray()
  
  // x is appended to. After this point, we know that x is unique.
  // We want to preserve that property.
  x.append(5)
  
  // We create a new variable y so we can write an algorithm
  // where we may change the value of y (causing a COW copy
  // of the buffer shared with x).
  var y = x
  longAlgorithmUsing(&y)
  // We no longer use y after this point, so we tell the
  // last use to take ownership.
  consumeFinalY(take y)

  // x will be unique again here.
  x.append(7)
}

Note that take y does not directly correspond to a retain or release of y. By shortening the lifetime of the value of y, the compiler is allowed to avoid retains and releases it might otherwise have made to keep a copy of y available in the caller. The compiler may or may not release the value of y at the point of take y, depending on whether the use site is able to take ownership of the value or not.

The take y operator syntax deliberately mirrors the proposed ownership modifier parameter syntax, (x: take T), because the caller-side behavior of the operator is analogous to the callee’s behavior receiving the parameter: the take y operator forces the caller to give up ownership of the value of x in the caller, and the take T parameter will assume ownership of the argument in the callee. They can be used in tandem to forward ownership of a value across call boundaries:

func +(_ a: take String, _ b: String) -> String {
  // Transfer ownership of the `self` parameter to a
  // mutable variable
  var result = take a
  // Modify it in-place, taking advantage of
  // uniqueness if possible
  result += b
  return result
}

// Since each result should be uniquely-referenced,
// this append chain will run in linear rather than
// quadratic time
"hello " + "cruel " + "world"

Using take in a callee’s function parameter declaration doesn’t by itself force the caller to let it take ownership, so this definition of + can also still be used in normal Swift code, which will still copy values as needed to keep them alive. take can be used in expressions involving + to require the ownership forwarding chain to continue:

var foo = "hello "
// Copies `foo` to keep the existing value alive for further access
var bar = foo + "cruel " + "world"
foo += "beautiful " + "world"
// forward ownership of the final `foo` value
var bas = take foo + ", i felt the rain on my shoulder"

borrow operator

When performing reads of shared mutable state, such as class stored properties, global or static variables, or escaped closure captures, the compiler defaults to copying when it is theoretically possible to borrow. The compiler does this to maintain memory safety and minimize the opportunity for the law of exclusivity to trip, in case other code tries to write to the same objects or global variables while a call is ongoing. For example:

var global = Foo()

func useFoo(x: Foo) {
  // We would need exclusive access to `global` to do this:
  
  /*
  global = Foo()
   */
}

func callUseFoo() {
  // callUseFoo doesn't know whether `useFoo` accesses global,
  // so we want to avoid imposing shared access to it for longer
  // than necessary. So by default the compiler will
  // pass a copy instead, and this:
  useFoo(x: global)

  // will compile more like:

  /*
  let copyOfGlobal = copy(global)
  useFoo(x: copyOfGlobal)
  destroy(copyOfGlobal)
   */
}

Although the compiler is allowed to eliminate the defensive copy inside callUseFoo if it proves that useFoo doesn't try to write to the global variable, it is unlikely to do so in practice. The developer however knows that useFoo doesn't modify global, and may want to suppress this copy in the call site. An explicit borrow operator lets the developer communicate this to the compiler:

var global = Foo()

func useFoo(x: Foo) {
  /* global not used here */
}

func callUseFoo() {
  // The programmer knows that `useFoo` won't
  // touch `global`, so we'd like to pass it without copying
  useFoo(x: borrow global)
}

borrow global suppresses the local copy of global and passes the reference as-is to useFoo. If useFoo did in fact attempt to mutate global while the caller was borrowing it, the attempt would trigger an exclusivity failure trap at runtime.

It is useful to be able to borrow parts of an object, such as a reference to an instance property, or a struct field within a property (by contrast with take x, which passes ownership of an entire value, and therefore only makes sense to apply to variables):

final class A {
  var x: B
}

struct B {
  var y: String
}

foo(borrow a.x.y) // borrow access to a.x, then borrow .y out of it

When working with local variables, the compiler can statically tell when borrowing in place is safe, so borrow isn’t strictly necessary. It can still be used as an annotation to guarantee that in-place borrowing occurs, and to have the compiler raise errors in cases where it isn’t possible:

func read(_: borrow String, andModify: inout String) {}

func doStuff() {
  var x = "foo"
  read(x, andModify: &x) // will copy x to avoid exclusivity error passing the first argument
  
  read(borrow x, andModify: &x) // error: nonexclusive use of &x
}

The borrow expr syntax intentionally aligns with the proposed ownership modifier syntax for parameters, (x: borrow T), because the caller-side behavior of borrow expr is analogous to the callee’s handling of the parameter value. A borrow T parameter declares that the callee will borrow the value it is passed from the caller, but does not by itself prevent callers from using a copy as the passed value. Using borrow expr at the call site forces the caller to pass the borrowed argument in-place.

@noImplicitCopy attribute

There are many circumstances when it’s desirable to suppress implicit copying altogether, and treat particular values as being move-only without making their type completely uncopyable. We could provide an attribute that can be applied to function parameters and variables to mark that variable as not being copyable:

func update(@noImplicitCopy array: inout [String]) {
  array.append("hello")
  array.append("world")
  
  let x = array // error: forces a copy of `array`
  array.append("oh no")
}

If a type is copyable, but copying the type is expensive or otherwise best avoided (because it’s large, contains a lot of reference-counted fields, involves C++ copy constructors, etc.), it could be useful to tag a type as no-implicit-copy, to make all variables that are (concretely) of the type no-implicit-copy:

@noImplicitCopy struct Gigantic {
  var fee, fie, fo, fum: String
}

func operate(on value: Gigantic) {
  var newValue = value
  newValue.fee += newValue.fie
  operate(on: value, and: newValue) // error: forces a copy of `value`
}

Or you might want to suppress copies generally within a scope, such as a hot loop:

for item in items {
  @noImplicitCopy do {
    ...
  }
}

Note that, unlike true move-only types, all of these would be local, non-transitive restrictions. A no-implicit-copy variable can still be passed to another function that may copy it:

func foo(@noImplicitCopy x: String) {
  bar(x: x)
}

func bar(x: String) {
  // OK to copy `x` in `bar`
  var y = x
  y += "bar"
  print(x, y)
}

And concrete types marked as no-implicit-copy would be implicitly copyable when used as generic or existential types (unless those generic values are also marked no-implicit-copy):

func duplicate<T>(_ value: T) -> (T, T) {
  // OK to copy value in a generic context
  return (value, value)
}

duplicate(Gigantic()) // this call is OK

This allows developers to apply @noImplicitCopy locally in their own code without cutting themselves off from the greater Swift ecosystem, which would need to retrofit annotations in order to support a truly transitive “move-only value” constraint, or imposing transitive constraints on their clients. Move-only types will be the language tool for transitively preventing copies.

Because the language semantics can’t copy a no-implicit-copy variable, such variables have “eager move” lifetime semantics, meaning their lifetime ends after an operation takes ownership of their value, regardless of whether the take operator is used:

func consume(_: take String) {}

func doStuff() {
  do {
    var x = "hello"
    x += " world"
    consume(x)
    print(x) // This is fine, we'll copy `x` to prolong the local var's lifetime
  }
  do {
    @noImplicitCopy var x = "hello"
    x += " world"
    consume(x)
    print(x) // ERROR: x used after take. we're not allowed to copy it
  }
}

No-implicit-copy values are also always borrowed in-place when used as borrowed arguments. So certain formulations may trigger static or dynamic exclusivity errors that would be accepted quietly for copyable values:

func read(_: borrow String, andModify _: inout String) {}

func doStuff() {
  do {
    var x = "hello"
    x += " world"
    // This is fine, we'll copy `x` to sidestep the exclusivity error that'd arise
    // from trying to simultaneously pass x by inout and by borrow
    read(x, andModify: &x)
  }
  do {
    @noImplicitCopy var x = "hello"
    x += " world"
    // ERROR: attempt to exclusively access x during a borrow
    read(x, andModify: &x)
  }
}

Values of move-only types would always have these “eager move” lifetime semantics as well.

Explicit copy operation

Without the ability to copy implicitly, an obvious question is, how do we explicitly copy? Because @noImplicitCopy isn’t transitive, it is easy enough to write a function that copies on behalf of its caller:

func copy<T>(_ value: borrow T) -> T { return value }

func doStuff() {
  @noImplicitCopy var x = "hello"
  x += " world"
  read(copy(x), andModify: &x)
}

But by analogy to take x and borrow x, we may want to make copy also be a contextual operator:

func doStuff() {
  @noImplicitCopy var x = "hello"
  x += " world"
  read(copy x, andModify: &x)
}
25 Likes

Thank you for writing this up! I think the design has come a long way from the originally proposed move function.

In terms of naming, is there a reason we went for @noImplicitCopy instead of something more straightforward such as @explicitCopy? I guess if we followed Val’s precedent (who have @implicitCopy), the proposed naming would make sense.

Another suggestion would be to have an attribute that reenables implicit copies in an explicit-copy context, such as @implicitCopy. But I was thinking that maybe explicit copies could be automatically enforced in some contexts. For example, when users explicitly mark function parameters as borrow and take, it’s likely that they’re writing performance-critical code, where implicit copies can incur significant performance penalties. Though, I do realize that this presents problems both with how explicit is interpreted and how borrow and take relate to inout.

One example where explicitly writing these modifiers shouldn’t result in explicit copies is protocol witnesses. There are already protocol methods annotated with __consuming and other ownership modifiers in the standard library. If, after ownership modifiers are officially in the language, autocompletion honors these modifiers and presents them in protocol witnesses, a lot of library consumers that just want to conform to a protocol may get caught up in dealing with ownership. However, I think that if we allow protocol witnesses to not have the ownership modifiers of their corresponding requirements, that automatic explicit copies could work.

Another problem with automatic explicit copies is that the distinguish borrow and take from inout, despite all being ownership modifiers. Although they all show how values are passed between functions and bindings, inout cannot be hidden away through Swift’s implicit copies. Swift uses value-semantics at its core, which require explicit mutation through a mutable binding (var) or inout parameters/mutating methods. However, if a value cannot be taken when passed into a function or binding, an implicit copy can and is already used.

Thus, I think that most people will not and should not encounter ownership modifiers, at least when first learning Swift. Swift’s implicit copies and COW model allow for very quick prototyping and ease the language learning curve. However, people working in performance-sensitive domains should be aware of Swift’s implicit-copy behavior and its performance impact, and by extension should have knowledge of ownership modifiers to control how values are passed. Hence, it seems reasonable to have automatic explicit-copy contexts on functions that have take or borrow parameters, since the people specifying these modifiers value performance.

2 Likes

I don't think any of us is attached to the name @noImplicitCopy. @explicitCopy definitely seems nicer. We're open to other suggestions.

5 Likes

As with the previous versions of this pitch I'd like to see a word or two of explanation why lexical scope (nesting) is inappropriate here as a way of explicitly ending variable lifetime:

x.append(5)

do {  
    var y = x
    longAlgorithmUsing(&y)
    consumeFinalY(y)
    // y is guaranteed to end here
}

x.append(7)

It is occasionally important for performance reasons to end the lifetime of a take parameter before the end of the function. (I agree that this is worth mentioning explicitly, and that it's harder to justify without having explicit take parameters in the language yet.)

1 Like

There are a few reasons not to rely only on lexical scope to end value lifetimes:

  • We don't want to impose "pyramid of doom" situations when you have more than one value whose lifetime you need to cut off.
  • Value lifetimes don't necessarily need to nest. var x = foo(); var y = bar(); consume(take x); consume(take y) should be valid.
  • It is useful to be able to take the current value out of a mutable variable and reinitialize it later.

Rust 1.0 originally only had strictly scoped value lifetimes, and this was a significant ergonomic constraint until "non-lexical lifetimes" were added later, which allowed for value lifetimes to shrinkwrap to their actual duration of use. (We had originally tried that same shrinkwrap-to-last-use rule in Swift, too, actually, but found that it broke idiomatic Cocoa code that makes heavy use of weak references too aggressively.)

7 Likes

Joe, these are great answers, thank you, please include some wording to that effect into the actual pitch once it is in the form of actual pitch.

I can see these two arguments:

  • we can sort of do this today without nesting:
func test() {
    var x: [Int] = getArray()
    x.append(5)
    var y = x
    longAlgorithmUsing(&y)
    consumeFinalY(y)
    guard let y = drop() else { fatalError() }
    x.append(7)
}

func drop() -> Void? { () }

although I admit that this is cheating / abusing and not as ergonomic as your explicit "take x" or "drop x".

  • The second one is harder to swallow – and I ask for an apology for repeating this argument from the previous discussions (just I never saw it answered) – the fears of "pyramid of doom" are understandable, just I can see how a similar line of reasoning can justify the introduction of "goto" statement ("makes language simpler"; "instead of all those "while's" / "for's", braces and scopes / "pyramids of doom" just have "if" and "goto""; "allows for an ultimate flexibility impossible to express via "structured" alternatives"; "language X has it and it works great in there".)

I'd like to see some strong motivated example that makes the superiority of explicit "take" / "drop" approach obvious. Hopefully people here who are familiar with Rust or other language with a similar feature could come up with some killer example.

1 Like

Just a short note here but I think this will be really valuable for us working on swift-nio. The number of circumstances where we really have to wrestle with ownership carefully is low, but there are a few places where we'd really benefit from giving the opportunity to our users to make their lifetimes clear, as well as in our own implementations. I think this proposal is clear and pragmatic, so I'm a strong +1.

4 Likes

Looks like you are in a good position to provide that killer example..

Have a look at this one:

func test() {
    var x = ValueType(1)
    print("x.append(5)  -->")
    x.append(5)
    var y = x
    longAlgorithmUsing(&y)
    consumeFinalY(y)
    print("x.append(7)  --> ")
    x.append(7)
}

It is the same as in the pitch above, just making use of some explicit types to see the internal nuts and bolts:

The value / ref types used.
class RefType {
    var value: Int
    
    init(_ value: Int) {
        self.value = value
    }
}

struct ValueType {
    private var ref: RefType!
    
    init(_ value: Int) {
        ref = RefType(value)
    }
    
    mutating func append(_ value: Int) {
        if isKnownUniquelyReferenced(&ref) {
            print("  quick assignment")
            ref.value = value
        } else {
            print("  COW assignment")
            ref = RefType(value)
        }
    }
    
    mutating func drop() {
        ref = nil
    }
}

func longAlgorithmUsing(_ v: inout ValueType) {}
func consumeFinalY(_ v: ValueType) {}

As written the above example would produce the expected (and unwanted) sequence:

x.append(5)  -->
  quick assignment
x.append(7)  --> 
  COW assignment

Then we introduce this extra line:

func test() {
    var x = ValueType(1)
    print("x.append(5)  -->")
    x.append(5)
    var y = x
    longAlgorithmUsing(&y)
    consumeFinalY(y)
    y.drop()  // **************
    print("x.append(7)  --> ")
    x.append(7)
}

and the log changes to the desired:

x.append(5)  -->
  quick assignment
x.append(7)  --> 
  quick assignment

Just note that in this case the "drop" is merely a function of "ValueType":

struct ValueType {
...
    mutating func drop() {
        ref = nil
    }
}

and no language change was needed to achieve the desired "non COW" behaviour. What am I missing? Is it not just a matter of agreement for value types to provide a function called "drop" and changing std types to adopt that convention?

As before, I would rather we found a different name for "copy"; it's a highly overloaded term, even in Swift. For example, "copy on write" refers to a different kind of copying, as do established Cocoa APIs such as NSCopying and NSMutableCopying.

I'm also very much -1 on the name @noImplicit{Anything} - Since it is phrased as an opt-out, it gives me the impression that the compiler typically does something undesirable, at least in some situations; and that I can't trust the language as it works by default. It feels like @noNannying or @noHandholding. It immediately makes you think "why would somebody want to disable this?".

I think @explicit{Copy/Lifetime/Ownership} is preferable; I don't feel it has such a connotation that the default behaviour is incorrect or undesirable. It's very... well, explicit about what it is trying to achieve - that you want to make a value's lifetime events visible in code, even if the default behaviour happens to also be correct.

At least, that's how I read it. Perhaps not everybody reads it like that.

4 Likes

Counterpoint: I think it's plausible to read @explicitCopy as a marker of an explicit copy rather than a marker that requires explicit copying. IOW, in isolation, @explicitCopy seems like it could be another proposed spelling for the copy operation.

In that sense I think @noImplicitCopy is better at describing what it's actually doing (disabling the default implicit copy behavior).

That doesn't mean that the polarity of @explicitCopy is incorrect, but I think something like @requireExplicitCopy would more accurately describe what's going on here, even if it is a bit more verbose.

6 Likes

The reason I said @explicit{Copy/Lifetime/Ownership} is because I'm not a fan of the word "copy" being used to describe this operation at all. Either of the latter two - @explicitLifetime or @explicitOwnership, are what I would prefer.

Whether or not they are also confusable with the operation itself depends on what the operation ends up being called.

2 Likes

That’s how @explictCopy sounds to me too. How about @explictCopying to mark explicit copying?

1 Like

I agree with that sentiment.

Would it make sense if we called this operation "duplicate" instead of "copy"? The operator could be called dup for short:

  • dup: contextual operator to create a duplicate
  • @overtDup: dup must be explicit
  • @noDup: duplications are forbidden (move-only), for a future proposal
func doStuff() {
  @overtDup var x = "hello"
  x += " world"
  read(dup x, andModify: &x)
}

Also, I think dup x should be a warning when the variable or type is not @overtDup (possibly with a fix-it to add @overtDup to the variable). I don't think there is a point in using dup, other than making unnecessary duplicates, when the compiler is managing it for you.

Was that hard to read? ... same thing reworded with "copy":

Also, I think copy x should be a warning when the variable or type is not @noImplicitCopy (possibly with a fix-it to add @noImplicitCopy to the variable). I don't think there is a point in using copy, other than making unnecessary copies, when the compiler is managing it for you?

The thing about the word copy is that we're actually talking about something even shallower than a "shallow copy". let y = copy x does not actually construct a new object; it constructs a new reference. Basically, it's a retain, and @noImplicitCopy is really "no implicit retains" -- it makes certain ARC operations, which are otherwise invisible, visible.

To underscore that point, consider that because NSCopying exists, we will have objects where:

someFunction(x.copy())
someFunction(copy x)

Have entirely different semantics. NSCopying declares that it produces an "independent object", and as such we can expect it to stop the sharing of mutable state:

a copy must be a functionally independent object with values identical to the original at the time the copy was made

NSCopying documentation

copy x (the operator being discussed in this proposal) does not produce an independent object, and that is not its goal - it exists to extend the lifetime of an existing object. The result of copy x may still share mutable state with x.

We also use the word copy in Obj-C properties in a way that aligns with NSCopying:

copy

Specifies that a copy of the object should be used for assignment.
The previous value is sent a release message.
The copy is made by invoking the copy method. This attribute is valid only for object types, which must implement the NSCopying protocol.

assign

Specifies that the setter uses simple assignment. This attribute is the default.
You use this attribute for scalar types such as NSInteger and CGRect.

retain

Specifies that retain should be invoked on the object upon assignment.
The previous value is sent a release message.

The Objective-C Programming Language

This is just the confusion we can expect within the Cocoa/Obj-C ecosystem, and among developers used to that terminology and way of thinking ("copy produces an independent object"). We should also consider what C++ developers will expect when writing copy x on an imported class (if they are managed by ARC in some way).

This is only true for reference types. Copying a value certainly produces an independent value.

I don't think there's any escape hatch there. A value may contain a reference, in which case @Karl's point is still valid.

It seems to me that this isn't just about retaining objects. In principle, @noImplicitCopy refers to any aspect of the value that the compiler might regard as duplicative in normal use, and which might be worth expending effort to avoid.

For example, it might be that the stack-based memory for a value might be regarded as a precious resource, so that it might be undesirable make additional stack copies of some very large struct.

It's been a truism of Swift that values of value types (unless modified) and references to objects of reference types exist in multiple interchangeable copies. This proposal introduces the idea of non-interchangeable copies, a distinction that exists regardless of the value/reference divide.

This looks great to me overall! Very nice to have a more complete picture of the ownership story related to take.

This as the headline motivating example of take remains unsatisfying to me. The text says take is introduced to solve the problems that 1) there is no "strong guarantee" that the compiler will optimize the lifetime of y to end before the final use of x and 2) future modifications of the code may introduce uses of y that inadvertently extend its lifetime. But as far as (1) goes, it seems even with take we have no such "strong guarantee":

And (2) still seems problematic since it does nothing for introducing new uses of x that are interleaved with the 'critical section' where y is alive, e.g.:

func test() {
  var x: [Int] = getArray()
  x.append(5)
  
  var y = x
  longAlgorithmUsing(&y)

  x.append(6) // non-unique!

  consumeFinalY(take y)

  x.append(7)
}

When I raised this previously it was pointed out that the solution I was reaching for seemed similar to the borrow variables discussed in the updated ownership roadmap, but that still leaves me with questions in regards to take. Is some form of borrow variables the intended solution here? If so, why is this example being used to motivate take?

Ought we have some sort of marker in these cases at the point where the non-transitivity of @noImplicitCopy causes us to lose its guarantees? It seems a bit silly to provide the functionality of @noImplicitCopy and also allow it to end up getting silently discarded.

And somewhat relatedly, is copy intended to guarantee that the compiler really will emit a copy operation, or does it just serve to allow the compiler to emit a copy when necessary? If the latter, would it make sense to just use copy as that marker to indicate that you may end up copying the value by passing it somewhere that loses its @noImplicitCopy-ness?

The example does not directly defend either of these claims...

Language rules give us strong guarantees about the static lifetimes of variables. Language rules don't give us strong guarantees about when copies that alias become unique again, because that information doesn't exist statically. Once you have separately owned aliasing references, you can't prevent someone else from introducing code that mutates one and copies the elements.

I do think the example makes it easy to understand how take can avoid copying CoW elements. And there's nothing wrong with relying on reasonable optimizer behavior. Of course, you could insert paranoid optimizer controls if you felt inclined:

  consumeFinalY(take y)
  withExtendedLifetime(x) {}
  x.append(7)

or

  consumeFinalY(take y)
  lifetimeBarrier() // DIY barrier
  x.append(7)

It would be interesting to talk about adding "lifetimeBarrier" to the standard library. That's entirely tangential though. Again, I don't recommend actually writing this code!

It's easy to defend the two main claims if we just focus on eliminating copies. Real-word cases frequently involve forwarding. Here, take is needed to avoid a reference copy (grep for __owned or __consuming in the stdlib code to see many places that need take):

  public func isSuperset<S: Sequence>(of possibleSubset: take S) -> Bool
   where S.Element == Element {
     if let s = possibleSubset as? Set<Element> {
       return isSuperset(of: take s)
     }
     //...

Avoiding useless copies is important enough on its own to justify the proposal. But it's more compelling if we bring it back to CoW performance. Avoiding copies in general can indirectly avoid copying all the elements in a CoW container. Here's a really terrible example off the top of my head:

func doNotCopyArray<T>(membership: inout Set<T>, array: inout [T], value: T?) {
  let subset = value != nil ? array : []
  //...
  if (!membership.isSuperset(of: take subset)) {
    array.append(value!)
  }
  //...
}

I'm sure someone can think of something less contrived.

1 Like

Sorry, I've been a bit unclear. I think we're saying very similar things. My point is more of a rhetorical one: I think using this CoW example as the primary motivating example for take is confusing precisely because it does not directly defend either of the claims that are used to introduce it. This CoW example was the only example raised in the Motivation section of original take pitch, and I think treating it as the main motivation for take muddies the waters.

That take also provides a way to avoid CoW triggering in the face of potentially-aliasing variables is a nice additional benefit, sure, but IMO it's better if the primary motivation for take are the situations where take is needed to avoid pessimistic copies.

1 Like