`borrow` and `take` parameter ownership modifiers

I would think @dynamicCallable is testament to that. It was intended for easier inter-op with Python (and other scripting languages), but has found wider use.

2 Likes

My goal was to only mention it as a future direction, and primarily to point out that the functionality of suppressing deinitialization and destructuring a value fits within the contract of a taking func already and doesn't require a new kind of parameter ownership to support. But you're right that it would fit more thematically in the take operator proposal, so I'll move this section there.

2 Likes

In preparing this for going into review, I have one concern, which is around the intersection of generics and move-only types.

  • The proposal describes how you'll be able to annotate protocols with take and borrow. it also describes how functions that take can fulfill a protocol requirement that borrows. When you're thinking about these annotations as just discretionary performance improvements, that makes total sense.
  • The proposal also describes how "the take versus borrow distinction becomes much more important and prominent for values that cannot be implicitly copied." and provides an example of a moveonly FileHandle which can be borrowed by a read function. This makes sense for a function that takes a concrete moveonly type.

But when you combine these two things together the story is missing bits. Protocols exist mainly to allow for generic programming, but we don't yet have a design for a way to work with moveonly types generically. We need more – like for the generic type to be designated moveonly and the read function to guarantee no copies of its argument.

I expect when these pieces do come along, they'll all slot in nicely but it would be good to get some discussion of them into the future directions, or in some wider vision document it can reference, to build confidence in that.

7 Likes

When we integrate move-only types into the generics system, I imagine we'll treat generic parameters, protocols, and associated types as they're written currently as implicitly requiring a Copyable constraint, so move-only types would not be able to be passed as a normal unannotated T, conform to an unannotated protocol, or satisfy an unannotated associated type requirement. There would then be a way to mark declarations to drop this requirement and allow move-only types to conform. As a strawman, we could write such a protocol as moveonly protocol:

moveonly protocol P {
  moveonly associatedtype A
  taking func takesSelf()
  func borrowsA(x: borrow A)
}

Move-only types cannot be implicitly copied, so any methods in a concrete type that satisfy the protocol's parameter requirements using a move-only must exactly match the protocol's ownership requirements. So a conforming move-only type must match the taking/borrowing/mutating-ness of the methods in the protocol:

moveonly struct Foo {
  // OK since ownership of `self` matches:
  taking func takesSelf() { ... }
  // Would be an error since ownership mismatch requires implicit copy:
  /* borrowing func takesSelf() { ... } // ERROR */
  
  // The associated type is satisfied by a copyable type:
  typealias A = String
  // so it's OK for the method to have different ownership from the requirement:
  func borrowsA(x: take String) { ... }
}

And similarly, any parameters that are move-only types must match the ownership of the parameter in the protocol requirement:

struct Bar { // not move-only
  // `self` is copyable, so the `borrowing`/`taking`-ness can change
  borrowing func takesSelf()

  // The associated type is move-only:
  typealias A = Foo
  // so the modifier for parameters of type `A` must match:
  func borrowsA(x: borrow Foo)
  // but it would be an error for it not to:
  /* func borrowsA(x: take Foo) // ERROR */
}

I can add that explanation to the Future Directions section.

1 Like

I assume that you mean here moveonly struct Foo: P (i.e., that you are writing about how Foo may conform to P).

Can you spell out a little bit how it is that you're envisioning that the taking/borrowing/mutating-ness of requirements in a moveonly protocol must match the conforming implementation, but (as you show here) the moveonly-ness of a moveonly associatedtype requirement doesn't have to be matched with a moveonly type to satisfy the requirement?

moveonly isn't a requirement, it's the taking away of a requirement. An unannotated associatedtype requires copyability, and the moveonly annotation removes the copyability requirement, but doesn't impose a negative requirement that the type can't be copyable.

11 Likes

I don’t think this is the right approach. Movable or a similar marker protocol should be implicit for all types, as I think we can all agree. Copyable should refine this protocol, to also support copying. So expressing that a protocol is moveonly doesn’t makes sense, as it implies a negative protocol constraint. Instead, what we want to say is that conforming types can drop the implicit conformance to Copyable. So it should entirely up to a type weather to be moveonly. Now, if we allow negative conformance constraints in general, it would be fine to have protocols designated for moveonly types, but I think moveonly protocols are currently a big leap.

2 Likes

It seems I am one of the very few people here who likes the verbs, i.e. “take” and “borrow”, which gives a mental picture of what should happen when the function is being called (as an imperative), and both are short (and easily pronounceable for non-English-native people, please do not forget about this). Of course, when using “take”, you then would have to say “this function has those arguments” for the common case.

I don't like "moveonly" as the name of the marker, but if you use that protocol in generic constraints, there has to be a way you can tell the compiler that instances of the generic type can't be copied.

1 Like

I'm sure that the proposers have considered this point, but just in case they haven't: the name “take” to denote argument consumption is an invitation to confusion, since we normally say of (_: Int)->String that it “takes an Int and returns a String.” In the Val project we spent a long time looking for a good way to describe consumption. Sean Parent's suggestion of sink seems to work really well for us. Just sayin'.

8 Likes

The compiler would still prohibit copying if a protocol is marked as non-Copyable. The implicit Copyable constraint would be gone and so would implicit copying within extensions or generic methods using that protocol. But a copyable type conforming to the protocol could have methods that utilize implicit copying.

1 Like

It’s not going to fly to make the default non-Copyable. The default has been Copyable for the entire existence of Swift and inverting the polarity will break virtually every protocol used for generic constraints. This would be the best design if we started from nothing, but we’re bot starting from nothing.

1 Like

let’s not conflate “easy to pronounce in my particular language” with “easy to pronounce for non-English-native people” in general. i myself greatly prefer owned and shared, since that is what these keywords are already named and what people who have been using these features in the real world as they currently exist in the compiler calls them.

I think a code example will help clarify what I meant before. We could have an implicit-conformance removal constraint: !: that indicates that a type does not need to conform to a protocol.

struct Vector !: Sendable { let x, y: Double }

func f(_: some Sendable) {}
f(Vector(x: 0, y: 0)) // ❌ Error

Now, in the case of move-only types and protocols, we'd be able to opt out from the implicit conformance to Copyable:

protocol CopyableP: Copyable {} 
// ⚠️ Every protocol implicitly refines Copyable

protocol NonCopyableP !: Copyable {} // OK

extension NonCopyableP {
  func getCopy() -> Self {
    self 
    // ❌ Self does not conform to Copyable
    // 🔨 Do you want to mark the function as take?
  } 

  // We can still add the constraint
  func correctGetCopy() -> Self where Self: Copyable {
    self 
  } 
}

The main difference with moveonly is that types are not required to be move-only:

struct MoveOnly: NonCopyableP where Self :! Copyable {
}

struct MoveAndCopy: NonCopyableP where Self : Copyable {
}
1 Like

I'm not sure I like the idea of protocols having to explicitly allow conformances using a non-copyable type. I think I'd rather that all generic parameters had an implicit copyable constraint, and if the protocol is allowed to impose any requirement, it should only be an affirmation that conformers must be copyable, IMO.

So for protocols which are copyable-agnostic, it would be on whoever uses the protocol (the generic function/type) to decide whether they support non-copyable values. Copyable values are always supported, of course.

Protocol extensions have an implicit Self generic parameter, which would also have that implicit copyable constraint. But just like other users of the protocol, you'd be able to write an extension which also supports non-copyable conformers.

To me, that reads as though a conforming type must not conform, rather than that it doesn’t need to conform. Indeed, if we had this spelling, users would reach for it in spelling constraints after where for the former purpose. In the general case (with only the exception of implicit conformances, which are limited in number), one never needs to give explicit permission not to conform to something; it is simply not mentioned.

2 Likes

For comparison, Rust has a similar implicit trait Sized, where “may be Sized” is written ?Sized. There is not any in-language support for “must not be Sized”, but that’s a concept that applies to any trait, really; since types can add capabilities over time, trying to constrain a type parameter as !Clone or !Sized could result in different behavior between releases of a library that would otherwise be compatible.

(The corresponding thing here would be “a move-only concrete type may become copyable without breaking source compatibility, but not the other way around, except for in some overload scenarios”, and we already have this with “an implicitly non-Sendable concrete type may become Sendable without breaking source compatibility, but not the other way around”.)

7 Likes

This is exactly how it would work, yes. But Copyable is already an implied requirement of all types today, and we can't take that away now (and IMO, we wouldn't want to even if we could, since most coders should still be fine getting along with copyable types only most of the time). Like Jordan said, moveonly here is the moral equivalent of Rust's ?Trait syntax for taking away a normally-implied requirement. It isn't a negative requirement that the type not be copyable. But regardless of all that, I'm not proposing any of this now—my intent is only to explain the interaction of protocol requirements and ownership modifiers in the future when there are move-only types that conform to protocols, no matter how that manifests. The fundamental existence of Movable and Copyable requirements will still be there in any likely language design I can imagine for Swift.

5 Likes

It's a great step forward for the language to make these modifiers public, but I agree with several posts above that "take" is too general a word. I doubt most Swift developers would intuit that "take" means +1 and "borrow" means +0. As a beginner, I would have thought "of course this function takes a String" and not related it to ownership or ARC at all.

2 Likes

Since the concern about protocol witness matching doesn't necessarily require involving protocols or generics without copyability requirements, I tried to avoid speculating about syntax or semantics too much in the wording I added to the proposal:

When protocol requirements have parameters of move-only type, the ownership convention of the corresponding parameters in an implementing method will need to match exactly, since they cannot be implicitly copied in a thunk:

protocol WritableToFileHandle {
 func write(to fh: borrow FileHandle)
}

extension String: WritableToFileHandle {
 // error: does not satisfy protocol requirement, ownership modifier for
 // parameter of move-only type `FileHandle` does not match
 /*
 func write(to fh: take FileHandle) {
   ...
 }
  */

 // This is OK:
 func write(to fh: borrow FileHandle) {
   ...
 }
}

All generic and existential types in the language today are assumed to be copyable, but copyability will need to become an optional constraint in order to allow move-only types to conform to protocols and be used in generic functions. When that happens, the need for strict matching of ownership modifiers in protocol requirements would extend also to any generic parameter types, associated types, and existential types that are not required to be copyable, as well as the Self type in a protocol that does not require conforming types to be copyable.

1 Like