Non-Copyable ownership requirement

I’ve been playing around with non-Copyable types, and I discovered something that I do not quite understand.

struct NonCopyable: ~Copyable {
  consuming func eat() {
  }

  // if this is allowed...
  func a() {
  }

  borrowing func b() {
  }
}

// .. why is an explicit "borrowing" required here?
func doStuff(with nc: borrowing NonCopyable) {
}

I’m trying to figure out if there are three possible states (inout, borrowing, consuming) here or four (that plus nothing at all).

Is a implicitly borrowing? And if so, why isn’t a parameter allowed to be the same? And if not, what does it mean to have something without an annotation?

1 Like

If not otherwise specified, func a() is borrowing.

3 Likes

Passing a noncopyable value in the parameter position only has 3 states like you mentioned.

For what it’s worth, Rust does it like this:
fn foo(x: NonCopyable) {} // consuming

fn foo(x: &NonCopyable) {} // borrowing

fn foo(x: &mut NonCopyable) {} // inout
1 Like

Thank you both! This makes sense to me except for the part where I’m not allowed to omit it from a parameter. Why is that required?

Having that requirement makes it much more obvious to understand the code (required = working with noncopyables) and its expected memory behaviorat least in my opinion.

Directly from the proposal:

1 Like

I’m not speaking for Joe or any of the other authors of noncopyables, but I assume it was just safer to be explicit at first and can revisit what an unannotated parameter behaves like at a later date. Considering you can omit borrowing from methods, it might make sense to make borrowing the default if you don’t specify, but that would also be confusing for folks coming from Rust etc.

2 Likes

Ah ha! Ok there we go. Now that I’m parsing the diagnostic more closely, it all lines up:

Parameter of noncopyable type 'NonCopyable' must specify ownership

There’s just a stronger emphasis on “parameter” here that I originally expected.

Broadly speaking, I have found the diagnostics for non-Copyable and non-Escapable types to be quite helpful and educational. The fixits for this particular issue, for example, are super cool.

Am I understanding right, that part of supporting non-Copyable types in the standard library means that all Swift users will now be more exposed to the borrowing keyword because it cannot be implicit in this particular case?

Not necessarily no. Optional was updated to support non-copyable wrapped values, so you can write Optional<NonCopyable>. Existing uses of optional with a copyable wrapped value don’t need to specify, but using an optional of a noncopyable does. If someone wants to be generic over an optional they can either use the existing spelling:

func foo<T>(_ t: T?) {}

or opt to be generic over noncopyables as well:

// must specify ownership here!
func foo<T: ~Copyable>(_ t: borrowing T?) {}

Users should only have to write borrowing when working with noncopyables (which the standard library is getting ready to add a lot of not already including Atomic and Mutex).

2 Likes

I should have been more clear, sorry. When I said “exposed” what I really meant is “see”. I just checked out Optional, which was a great pointer by the way. There’s a fair bit of consuming in that API, but only a tiny bit of borrowing.

However, I’m definitely starting to feel like it could improve both progressive disclosure and consistency if an implicit borrowing was permitted.

I actually think it was a mistake to make borrowing required for non copyable types, and I'd like to see it become implicit in future. When this feature was first introduced, it was very new and it wasn't obvious whether this would be unclear, so the more explicit form was chosen. The nice thing is you can always make being explicit optional, whereas going the other way is a problem requiring upcoming language features.

A middle ground may be to make it mandatory, but only for public interfaces, where getting it wrong has source-breaking consequences so you should be required to specify one way or the other. But for internal and below, just make it implicit – similar to how Sendable inference works.

4 Likes

This does seem inconsistent, because once we get to SIL where ownership matters, self is "just another parameter."

I also agree with Ben here:

The (admittedly few) times I played around with noncopyable types, I noticed that almost all of my function parameters were borrowing, and having to spell the keyword every time was kind of annoying.

3 Likes