I don't this restriction was actually mentioned above? But I'll assume it's something like
Ownership annotations on witnesses satisfying protocol requirements must match the annotations on the protocol declaration, e.g.
protocol Foo { associatedtype T consuming func f() func g(x: consume T, y: borrow T) } struct Bar: Foo { func f() {} // error: missing 'consuming' func g(x: borrow T, y: consume T) {} // error: 'x' should be 'consume', 'y' should be 'borrow' }
Sound reasonable?
The proposal doesn't actually say if a witness can be annotated. In fact, I don't actually think there's a way to get to make this API-compatible with the annotations-must-match restriction:
- if they can be annotated, then the protocol cannot have its annotations changed in a API-compatible way, as type that conforms may have annotations matching the old declaration, but these won't match (and will violate the restriction)
- if they can't be annotated, adding even a defaulted requirement may break code along the lines of @orobio's example, where there's a (previously) unrelated method that has annotations: adding the requirement makes this method a witness, but it has annotations!
This, along with not wanting to silently change behaviour as @orobio's example demonstrates, pushes me more towards no restriction. It seems feasible to have tooling (or a compiler flag/Xcode setting) that will flag mismatches between witnesses and protocol requirements on an opt-in basis.
The abstract idea of a protocol may not match all concrete implementations of it, because they may be more specialised or have "surprising" trade-offs (e.g. optimise one function to be O(1) at the cost of another becoming O(n2)). For instance, imagine a Bag
protocol (ala Collection
):
protocol Bag {
associatedtype Element
// presumably 'add' will usually store the value somewhere
func add(_ value: consume Element)
...
}
extension Set: Bag {
// 'add' does store the value into memory somewhere, and so
// benefits from 'consume'
func add(_ value: consume Element) { ... }
...
}
extension Array: Bag {
// Same as 'Set'
func add(_ value: consume Element) { ... }
...
}
struct StringTrie: Bag {
typealias Element = String
// this is a trie, and so never actually stores 'value': it is
// broken into parts so there's no point in consuming the value
func add(_ value: borrow String) { ... }
...
}
Concrete users of StringTrie
avoid retains and releases, and generic users pay "only" the cost of the extra function call of the thunk, since consume
->borrow
is cheap. I think the protocol has the correct annotation here, as it is optimising for the most common case (and, in a moveonly world, maintaining maximum generality of what types can conform to it), but the concrete type has extra constraints that allow it to relax the requirements.
In a world where distinctions between inout
, consume
and borrow
are enforced strictly there's a hierarchy: an owned value (consume
) can always have a mutable pointer (inout
) created to it for free and a mutable pointer can always be downgraded to an immutable one (borrow
) for free . This implies that concerns about the performance of copying don't apply to satisfying a consume
requirement with an inout
witness (definitely a bit weird, but it's theoretically okay), an inout
requirement with a borrow
witness (i.e. protocol is giving the option for mutation, but the concrete implementation doesn't need it) and, transitively, a consume
requirement with a borrow
one.
Of course, the cost of the thunk call itself could be problematic enough (although these very small thunks seem like they'd be pretty much just the cost of a call to a function in another dylib?).
I don't think this is a particular concern: it will always be statically known if a type could be a moveonly type (due to source compatibility, there can't be a way to retroactively make a type moveonly, or instantiate a generic parameter with a moveonly type unless the parameter is marked as "possibly moveonly" in some form), and so statically known when ownership annotations must match. I don't think there's much value in maintaining consistency with behaviour with copyable types, because this doesn't seem particularly different to, say, func f(_: consume T) {} func g(x: borrow T) { f(x) }
(that's also a mismatch in borrow
/consume
: that works with copyable T
and can't if it's moveonly), and there's numerous other ways in which moveonly types different in behaviour (e.g. let t = (x, x)
).
(This changed to borrow
very recently: Plan to change the convention of passed "Normal Parameters" from +1 to +0 - #9 by Michael_Gottesman)