Interaction between non-escapable types

Apologies for the multiple topics, but while I was attempting to make progress understanding Escapable parameters to non-escapable initializer, I have run into more questions.

I've add the points of confusion as comments. But, just to spell it out:

I do not understand why making A public has an effect on this code.

I do not understand why test(a3:) does not compile. I am also particularly confused by the reference of an Escapable type when all the types in use here are ~Escapable.

public struct A: ~Escapable {}
struct B: ~Escapable {
	// only an error if A is public?
	// error: Invalid use of borrow dependence with consuming ownership
	@_lifetime(borrow a1)
	init(a1: A) {}

	@_lifetime(copy a2)
	init(a2: A) {}

	// "Escapable"?
	// error: Invalid lifetime dependence on an Escapable result
	@_lifetime(borrow a3)
	func test(a3: A) {}
}

If anyone out there has any ideas on what's going on here I would really appreciate the help.

2 Likes

my initial guess (unverified):

a1) you need a borrowing parameter
init(a1: borrowing A)

a3) you don't need a lifetime annotation at all, because you are not returning anything

2 Likes

Afaik this is right. Initializers default to consuming their parameters unlike other functions. This is only significant for CC / ARC purposes normally, but with ~E types it means you need to specify borrowing if you want to depend on the ~E value you initialize with.

Fwiw we expect to change the lifetimes system somewhat substantially in the future to be more type based.

1 Like

Thank you both so much! Some quick follow up. This all makes a lot of sense to me. I'm getting there!

Are the effects of public possibly just a bug then?
Is the use of the term "Escapable" a bug in the diagnostic?

Also, some meta-feedback, in case anyone working on ~Copyable and/or ~Escapable is reading. I find it quite confusing that the following code disallows an (in my opinion) intuitive default of borrowing, but the initializers of ~Escapable allow an implicit default that is (again, of course, my opinion) very subtle.

struct NonCopyable: ~Copyable {}

struct User {
	// error: Parameter of noncopyable type 'NonCopyable' must specify ownership
	func take(_ nc: NonCopyable) {
	}
}

But I'm not sure how relevant this is given that the lifetimes system could be changing. Is there anywhere I could get more information on this?

Is it an intuitive default? After working with noncopyables pretty extensively in Rust, that feels very consuming to me. :laughing: It's not that initializers to ~E allow implicit defaults, all initializers for any kind of type defaults to parameters being consuming (which I am an avid opponent for implicit defaults for anything API surface wise).

2 Likes
1 Like

I have never used Rust and I'm super new to the concept of ownership/lifetimes, so I do not feel qualified to comment on that part. I dunno, I'm really just trying to figure out how it all works.

It seemed to me that borrowing is something I have been encountering as a default in many places. Maybe I'm wrong? Where else aside from init is consuming the default?

What I would really like to avoid is a situation where there is an implicit default, but that default is a) contextual and/or b) results in disclosure of ownership concepts in APIs used by programmers uninterested in ownership.

Edit: upon re-reading your response, it sounds like you in favor of explicitness in public APIs in this regard. So, in that case, we disagree.

(Given the comments in this thread, that seemed like at least a possible direction: Non-Copyable ownership requirement)

inits are the only place where parameters are consuming by default iirc. For every other function parameters are technically borrowing by default.

struct NE: ~Escapable {}

func foo(x: String) -> NE {
  NE()
}

This is valid because we pass the String as "borrowing". I will note and add to the confusion that if you explicitly marked the parameter borrowing String then the compiler will treat your string value as a pseudo-noncopyable value and disallow implicit copying:

func foo(x: borrowing String) {
  let y = x
}
error: 'x' is borrowed and cannot be consumed
1 | func foo(x: borrowing String) {
  |          `- error: 'x' is borrowed and cannot be consumed
2 |   let y = x
  |           `- note: consumed here
3 | }
4 | 

Noncopyables don't have a default because we wanted to make it very explicit in source what ownership you are expecting to receive. In a lot of cases you're right, borrowing is what you will reach for when receiving noncopyable types.

(And for reference, in Rust you can pass a type 1 of 3 ways 1. fn foo(x: T) takes T as a consuming value, 2. fn foo(x: &T) takes T as a borrowing value, and 3. fn foo(x: &mut T) takes T as a mutable borrow value.)

1 Like

Thank you so much for this explanation. I have noticed this pseudo-noncopyable behavior!

Just to further explain myself here, I completely get the idea that an API should be explicit. I like this concept, in principle. However, here, my assumption is that the vast majority of developers using Swift will never need to think about these details. So, if you will allow me to pick on Optional from the standard library.

public static func == (lhs: borrowing Wrapped?, rhs: _OptionalNilComparisonType) -> Bool

Advanced users are best equipped to tolerate an implicit default, and all other users benefit tremendously from the improved progressive disclosure. That explicit borrowing is helping those that don't need help, and hurting those that do.

But, this is all kind of tangential to the issue I originally ran into, which is that in this case, I really want the compiler to force me to write consuming for inits, because that would have really helped me understand what's going on.

I think this is the crux of my argument against implicit defaults is that if you don't know about them, they actively hurt you in the ownership world. That's why I don't agree with:

Because the explicit consuming would've helped! There's no source of confusion if the ownership is explicit and is something folks can easily lookup and find documentation on. (Although our diagnostics should help out a little more)

1 Like

Right, 100%! But that's because the default is currently contextual. If that was not the case, and, for example, the absence of ownership always meant borrowing, I would have understood the problem immediately.

I think "Escapable" here is referring to Void since that function doesn't return anything, maybe if the diagnostics mentioned the name of the type in question, that would make it more understandable.
something like "aka" in typealias diagnostics :man_shrugging:

typealias S = String
func f<T: AdditiveArithmetic>(_: T) {}
f(S()) // error: Global function 'f' requires that 'S' (aka 'String') conform to 'AdditiveArithmetic'
2 Likes

Ohh, of course, the result! Thank you so much!