[Pitch] Formally defining consuming and nonconsuming argument type modifiers

Brooding this over, I still don't see why we would need to include anything along this line. Compilers do that regularly; it's even known to change the function signature already. This will just add pedantry and get in the way of the discussion, especially since you normally don't observe the actual ref count (just whether it's zero).


But anyway, I'll stop.

1 Like

I’m not suggesting that you’ve gone ahead and invented the term out of whole cloth. However, we do need to evaluate the suitability of terminology in this time and context even if it has precedent in Objective-C.

As the discussion which has since unfolded reveals, Rust’s ownership mode has risen to occupy a prominent place in people’s minds. It would be appropriate to ensure that any terminology is conducive to learning (and mastery) by many audiences, not just those steeped in Objective-C, but also those who have no prior experience with ownership annotations and those who are experienced in Rust.

10 Likes

It's perhaps worth pointing out that the words "retained" and "unretained" (which @xwu mentioned before) have precedent from Unmanaged<T>.passRetained/.passUnretained.

In this case, of course, there is no risk of leaking/over-releasing, because you're just specifying a particular calling convention and the compiler will figure out how to balance it, but the words do have a similar conceptual meaning as they do in the Unmanaged APIs.

That said, I still prefer some variant of escaping/nonescaping. It looks less scary than "unretained":

extension MyInteger {
  init?(_ string: unretained String)   // Looks scary.
}

extension MyInteger {
  init?(_ string: nonescaping String)  // Less scary.
}
2 Likes

I’m not sure I agree with your scariness point, but I do think it’s important to make sure that this is never confused with unowned.

I’ve been staying out of this discussion, but I just want to chime in here to mention that I personally find the spellings of these two methods to be particularly unhelpful.

Every time I see them, I always wonder, “Wait, does passRetained mean I am passing a value which has already been retained, or one which I would like to be retained?”

And, “Does passUnretained mean I am passing a value which is currently not retained (and thus needs to be), or one which I would not like to retain (because it already has been)?”

Sometimes I look them up in the documentation and straighten it out in my mind for the moment, but invariably I forget again because the spellings emphatically do not convey to me the intended meanings. I find them both highly ambiguous.

Either one could be interpreted to mean either thing.

15 Likes

I absolutely have the same problem here too; no matter how many times I try, it ends up being no more illuminating and I treat it almost like a magical incantation that just has to be memorized.

I think the point is well taken though that the passRetained and passUnretained APIs deal with basically the same concept. Therefore, ideally, we'd discover a more intuitive name for these APIs as well!

(Honestly, of all the descriptions I've seen thus far, "passing the argument at +0" and "passing the argument at +1" makes the most intuitive sense to me...)

7 Likes

That does seem like it’d be better served with a single method and a parameter (either Bool or enumeration) for specifying which you want.

I didn't take it that way. I was just explaining where the prior art is from. That is all.

3 Likes

I find it hard to digest this pitch, and especially to take a stance on naming, without considering its relationship to move-only types. (This pitch shouldn’t depend on move-only types, or lock in decisions about them, but I think it should have a clear relationship to the idea.)

In particular, this pitch describes the existing uses of __owned in the standard library in terms of optimizing reference counting, but my understanding at the time was that it was important to lock in consuming semantics for things like Array.append in anticipation of move-only types – a move-only type can only be appended to a collection if ownership is transferred.

It does seem reasonable to me that in a generic context, “consuming” semantics means passing-as-+1 for reference counted types, and requiring a move rather than a borrow for move-only types. My instinct is that these constraints will usually be aligned, and requiring separate keywords/annotations for both in order to support exceptional cases where they aren’t would likely be an unnecessary syntactic burden.

If this is the anticipated direction for move-only types, then that should be considered in naming discussions; “retained” is out of the window in that case.

On the other hand, if it isn’t, the naming of this feature needs to leave room for unambiguously different terminology for move-only types, and “consuming” is probably not the best thing.

3 Likes

Here’s a question: would it be of any value to generalize this further by allowing explicit retain/release?

That might be overkill. It's unstructured and very easy to introduce bugs. That's essentially the goto of ARC.

I find it works best when we attach a higher concept, like ownership (of a variable or a reference) and work with that concept (e.g., taking ownership of a new reference, creating another reference and owning it).


The more I think about it, the more I think we should attach the consuming to escaping and nonconsuming to nonescaping (requiring escaping for general types). We'll need explicit consuming/nonconsuming overrides to interop with old binaries.

There are four scenarios depending on whether the argument escapes and whether the caller uses it afterward:

  • Argument escapes, the caller does not use it:
    marking the argument as consuming would be beneficial here,
  • Argument does not escape, the caller uses it: marking the argument as nonconsuming would be beneficial here,
  • Argument escapes, the caller uses it:
    marking would not help here; they'll just shift the extra retain needed toward the caller or callee, and
  • Argument does not escape, the caller does not use it:
    same as the above for release.

That's why I think we could use the escaping as a marker for consuming as we're either correct and reduce the ARC traffic, or we move the necessary retain/release around.

Admittedly, this is changing the heuristic, so the bar is much, much higher, but it might be a whole lot more beneficial in the long run if we can figure out the source-compatibility story.

4 Likes

I am strongly opposed to making any concessions towards support for unstable features.

As for the rest of it, I think it’s reasonable to make escaping/nonescaping imply consuming/nonconsuming.

No, not for an unstable feature, but the old heuristic. We need to be able to mark an argument as nonconsuming escaping so that they can be passed around into old binaries, e.g., as closure.

This is getting added in a major release, right? Is that sort of compatibility even expected?

Also, I don’t think changing the heuristic is part of this pitch.

Yes, that was the entire point of making Swift ABI stable in the first place.

3 Likes

Actually, there's a small benefit in using consuming for this case: it allows the argument to be released earlier, as soon as the callee has finished using it. So nonescaping consuming could be beneficial here, although it won't change ARC traffic.

3 Likes

Thanks for your info.

Out of curiosity, I wonder how about the convention for return values. Is a reference type return value still guaranteed to be returned at +1 nowadays? If so, do we have some modifiers/attributes to override that behavior?