Overall, I think this is a welcome improvement and I’m glad this is being proposed for the language.
From the proposal:
I think it should be noted that the existential inline-storage maximum is 3 words. That means if you create an existential containing a Borrow of a 4-word type, then it will be stored in an out-of-line heap allocation, even though an existential containing a borrow of a 3-word or 5-word type would store it inline. (Assuming, of course, that the 4-word type is bitwise-borrowable and not addressable-for-dependencies.) I think this is important to call out in the proposal (and maybe even the documentation) since it's a narrow edge case that can cause unexpected performance and memory usage problems, especially if a programmer thinks of it like a pointer type.
I also think we should have other versions of Borrow. It would be nice to have a version where you can specify the maximum bitwise-copy size so that people can create types that are friendly with existentials and other size constraints.
It would also be nice to have a version of Borrow where the pointer representation is always used. This would be especially nice in generic cases where the borrowed type will almost always be larger than 4 words and allowing the bitwise copy representation would cause unnecessary branching and code size bloat.
Even if they're not added with this proposal, I think it's worth mentioning other versions of Borrow as a future direction.
I think it's odd that Swift has these pairings:
borrowing and mutating (borrowing/mutating func doThing())
borrowing and inout (func doThing(_: borrowing/inout Type))
borrow and mutate (var property: Type { yielding borrow yielding mutate })
And now this proposal pairs Borrow with Inout. Having all of these similar-but-not-the-same pairings makes the language feel inconsistent and convoluted. I imagine this will be a big source of frustration to people who are trying to learn Swift. Perhaps, instead, we could call them Borrow and MutableBorrow? That would match Span and MutableSpan.
I also don't like the naming of the value property.
-
"Value" is not a very descriptive term. Every instance in Swift has a value. A name like dereferenced, wrapped, or projection would do a much better job of describing what the accessor actually does.
-
It's easy to mistakenly assume the term "value" implies value semantics. Personally, I try to use the term "instance" instead of "value" for things that don't (or might not) have value semantics to avoid confusion.
-
If Borrow and Inout are single-instance analogues to Span and MutableSpan, I think using an empty subscript emphasizes this better (borrow[] vs span[0]).
-
Because "value" is a non-descriptive term, adding it to Unique, Borrow, and Inout would create a language convention that must be learned by programmers. Programmers would have to learn just as much as they would if [] were used for this convention.
value is the worst of both worlds. It's not as short as [], and it's not descriptive like dereferenced, wrapped, or projection.
If we think that dereferencing/unwrapping wrapped objects will be common enough that it's worth making people learn a new thing in order to declutter code, we should use the empty subscript. This could benefit not just types like UnsafePointer, Borrow, Inout, and Unique, but also other types worth adding to Swift in the future, such as an equivalent to C++'s shared_ptr, a copy-on-write wrapper type, and a wrapper type that behaves like indirect. I think it's worth noting that the empty subscript operator has been reserved for this purpose since 2018 at least.
If it's not common enough, then we should use a descriptive name for the operation. This risks the property being repeated a lot throughout some codebases. But, with borrowing and mutating accessors being the preferred way to offer borrowing/mutating access, it could end up not being a big deal.