My concern is that code which needs to use this feature will be surrounded by other code that needs to use this feature; e.g. the core of an application’s hot loop. Will such pockets of code by peppered throughout with @nonmoving
and @noImplicitCopy
?
Although we only currently have it wired up for variable bindings, I think it'd also be useful to have @noImplicitCopy
scopes, which wouldn't completely eliminate the markup, but could mean that you have to only write @noImplicitCopy do { }
once around your hot loop.
I read through the pitch but its not clear to me if there's any way to mark a type as not being deinit
-able. Example use: say I had type like AsyncInterruptTrigger
which releases a suspension point elsewhere. I the library author want to enforce that the owner of one of these triggers either calls fire()
or cancel()
in order to consume the instance. How would you do that with this pitch?
EDIT: I realized this was discussed earlier as "linear vs affine types". I still think this is something worth mentioning in the pitch under alternatives considered or future directions or both.
I know this isn't being pitched, but do you see a future direction where a language mode could be applied to entire module to opt into this behavior? I see use in environments such as firmware HAL libraries where you might want to flip the defaults?
English language bikeshedding
Some dictionaries specify that "copiable" is the standard spelling for "able to copy", although the Oxford English Dictionary and Merriam-Webster both also list "copyable" as an accepted alternative. We prefer the more regular "copyable" spelling.
The negation could just as well be spelled @uncopyable instead of @noncopyable. Swift has precedent for favoring non- in modifiers, including @nonescaping parameters and and nonisolated actor members, so we choose to follow that precedent.
Others have mentioned the use of an @attribute
feels a bit off. I think this concept is more simple to explain syntactically to new users by leveraging existing precedent set with Sendable:
struct FileDescriptor { }
@available(*, unavailable)
extension FileDescriptor: Copyable { }
I think this has value if a language mode to switch the default is in the cards as making a type Copyable can also leverage the same familiar syntax.
// My module with some language mode flag like --disable-implicit-copyable
struct Foo { }
extension Foo: Copyable { }
Another possibility to reduce annotation noise might be for us to say that types implicitly lose their copyability when they do something that forces them to be noncopyable, like declaring a deinit
or containing a noncopyable field. We already do this with Sendable
for non-public
types, which are implicitly Sendable
if their components all are.
We're also discussing a @noImplicitCopy
attribute that can be applied to normally-copyable things to get fully manual copying behavior. Whether a type is copyable or not is a fundamental part of its representation, so it wouldn't work well to have types sometimes be copyable or not, so I think that sort of annotation, to tell the language not to implicitly take advantage of copying even if a copy operation is available, is a better way to go for the sort of situation you describe. The pitch that's up only discusses @noImplicitCopy
as a variable modifier, but I think we could support it for scopes as well, and one of those scopes could be "the entire module". However, one thing to work out with these scoped noImplicitCopy modifiers is whether there should be exceptions to allow some types to always be copyable. You probably never really care if an Int
gets implicitly copied, for instance, and it would be annoying to have to think about copying at that level.
I think this is really the right way to go. I'd expect it to be the common case that we care about copyability not just for a specific type but for regions of code or entire modules. I'd argue it's less ergonomic to have to express negative constraints repeatedly for individual arguments or types than to flip the defaults for regions of code (files, modules, or possibly sections of files using something like #pragma
); I think it'll be rare to want to think explicitly about copyability for one type or parameter but not for all within a scope.
I think it would make most sense to treat implicit conformance as a module-level attribute that's turned on by default, and that if you passed e.g. --disable-implicit-copyability
it would flip the default for that module. You could then re-enable it for sections of code that you haven't migrated or don't intend to migrate using something like @beginScope(ImplicitlyCopyable)
.
Under this sort of model, you don't need to consider negative constraints; you would write the code in a scope where the Copyable
constraint is assumed not to exist.
In this design, all existing modules would be treated such that all types automatically conform to Copyable
, all generic constraints have an implicit Copyable
constraint, and all protocols descend from Copyable
. In a scope that opted out of implicit copyability, those extra requirements would have to be spelled out explicitly, and all types and variables within that scope would need to be explicitly copied (assuming they conform to Copyable
) or moved.
With regards to types that we still want to allow implicit copying on: a module built without implicit copyability enabled could introduce its own conformances to a marker protocol ImplicitlyCopyable
for Copyable
types (e.g. extension Int: ImplicitlyCopyable {}
if Int
was not marked as ImplicitlyCopyable
in its defining module). It could also conform any types it vends to ImplicitlyCopyable
, which would also imply a Copyable
conformance. Any struct
s or enum
s that conform to Copyable
would not be allowed to have a deinit
.
As a side note: we could apply this same solution to Opt-in Reflection Metadata, since again that's a place where we're wanting to change the default for regions of code – in that case implicit conformance to Reflectable
rather than Copyable
.
I don’t think the problem here is the negative copyable constraint ?: Copyable
, but rather implicit requirements. I think a better name for your attribute is @implicitConstraint(MyProtocol)
, which —as you say— could generalize to other feature like reflection.
This syntax generalizes a lot better. Swift already synthesizes some implicit conformances (e.g. Sendable), so a general syntax would be a great way to opt out. The syntax would also naturally extend to opting out of the aforementioned implicit constraints. But more importantly, Copyable
should be a protocol. IIRC, the compiler generates witnesses for copyable types that describe how they should be copied. Even if Copyable
’s requirement is an underscore-prefixed __copy()
method, implementing and explaining copies in Swift would be more straightforward. So it only makes sense that a move-only type would shed its implicit Copyable
conformance, like any other conformance (?: Sendable
), with a ?: Copyable
syntax.
I think the key distinction to draw is that I’m suggesting @noImplicitCopy
be automatically applied to all code that isn’t in an ImplicitlyCopyable
scope – or, to put it another way, that code that isn’t in an ImplicitlyCopyable
scope doesn’t automatically synthesise conformances to ImplicitlyCopyable
for Copyable
types that are used within said scope (wherever they may be defined). That’s in addition to suppressing automatic conformance of Copyable
for all types defined outside of an ImplicitlyCopyable
scope.
Rephrasing without negative constraints: code within an ImplicityCopyable
scope automatically generates conformances to ImplicitlyCopyable
for all Copyable
types regardless of where they’re defined, and additionally synthesises conformances to Copyable
for all types defined within that scope. All current Swift code is assumed to be implicitly within an ImplicityCopyable
scope.
Yes, I also think this is very important for a large number of use cases for move-only types. I think I would want to make it possible to mark deinit
as public
, private
, fileprivate
, or internal
, because the type itself might just be a move-only token used as an API currency type.
This is very nicely subsetted out from the full feature set. Obviously I look forward to generics-compatible non-copyable types, but I agree that this has use cases on its own, and it is definitely easier to review these proposals in chunks rather than all at once, even as we treat them as part of a whole. Kudos!
People already seem to be discussing a number of things I'm interested in, especially "should the attribute affect the type's generic parameters" because that does affect future proposals. There's only one thing that's jumped out at me that no one's mentioned:
For a local
var
orlet
binding, orconsume
function parameter, that is not itself consumed,deinit
runs after the last non-consuming use.
What does "after" mean here? The example shows a non-consuming use in a function call, followed by deinitialization on the next line. What if there are nested function calls? Thus far, Swift does not share C++'s concept of a "full-expression", so would it happen between the inner and outer calls, like inout
ending/cleanup? If the function is inlined, could the deinit
happen before it's even complete? And if I want my binding to live longer, I can certainly use an explicit consume x
, but the cost of forgetting that could result in a bug.
Between the discussion about class instances being released unexpectedly early (which I'm having trouble finding at the moment), and the precedent set by Rust, I would appreciate more discussion on why the default behavior isn't to deinit at end of scope, with an early consume
allowing for more control when needed.
EDIT: I can think of one reason why this rule isn’t sufficient: directly passing an owned return value to a borrow
parameter, or discarding one. It would make sense to me if those behave like inout
while named bindings behave like defer
, just as struct members and enum payloads have their own ordering.
Yes please. A major reason to adopt move semantics is to end reliance on optimizer magic. It seems counterproductive for the default mode to defer to the optimizer’s lifetime analysis.
Sorry for not being clear. The intent is to specify that the value is destroyed immediately after its last borrowing use ends. So that's stricter than end of scope, but should still be a well-defined location, not subject to optimizer whims, since borrows of noncopyable values begin and end at well-defined places. Looking toward values with lifetime dependencies, which may be lifetime-bound to borrows or directly contain borrows of other values, I expect that shrinkwrapping the lifetimes would get us closer to Rust's "non-lexical lifetimes" model, so code doesn't need to manually shorten lifetimes to avoid interfering borrows when values linger. I thought that was how Rust worked in general—is there a different rule for Drop
types?
On that note, another area of design here has to do with library evolution and deinits—do we want to allow for public types to add deinit
s without affecting API or ABI? Lifetime aside, the presence of a deinit also puts some restrictions on how code outside of the type can consume it—there needs to be a whole value for the deinit
to consume, so you can't partially destructure a value with a deinit by consuming some of a struct's fields or doing a consuming switch
on an enum
. Library evolution would also be a wrinkle in allowing for different lifetime semantics for types with or without deinits.
Yeah, non-lexical lifetimes only apply to references; other types still use the classic “end of scope” model. See 2094-nll - The Rust RFC Book
Just a naming thing: I find myself thinking about what @John_McCall said about naming conventions, and my own vague intuition about there being an lvalue/rvalue-like distinction here that naming should respect.
Looking at the code examples in context in this proposal, my gut feeling is that the keyword should be a gerund (consuming
/ borrowing
) in declarations:
func write(_ data: [UInt8], to file: borrowing FileDescriptor) {
^^^^^^^^^
func close(file: consuming FileDescriptor) {
^^^^^^^^^
That reads better to my eye. John was skeptical of the gerund, but darn it, in context that just flows off the mental tongue, as it were. The write
function writes to a file by borrowing a FileDescriptor
. It’s right there in the code.
I do also see the case for a past participle:
func write(_ data: [UInt8], to file: borrowed FileDescriptor) {
^^^^^^^^
The colon usually reads as “which is a”, and this naming fits: “Write data, which is a UInt8
array, to file, which is a borrowed FileDescriptor
.” Both those options read better to me than the imperative verb borrow
.
To be clear, the keyword should still be an imperative verb (consume
/ borrow
) when applied to an expression that supplies a value:
munge(borrow thinger)
^^^^^^
funge(consume thinger)
^^^^^^^
Trying to articulate my intuition here: one describes what will happen elsewhere whenever the thing is used; the other describes what does happen right there in the usage site. That's post hoc explanation, to be clear; I'm just reacting to the fluency of the code itself.
Perhaps? It's the code examples from this proposal I was referring to, though you're right that it's more on-topic for the other proposal. Feel free to move it (or let me know if I should).
I just think perhaps they'd like to hear about your opinions over there in that thread :)
munge(lend thinger)
in that case, or?! :-)
I had a similar thought. lend
/ loan
seemed like a bridge too far at first blush…but you do have a point!
You could argue for lend(ing)/borrow(ing) too…