I said it in the other thread too, but my general impression is that this
- is unnecessarily complex
- does a lot of "damage" to the language
Complexity
When I look at these use-cases, I'm largely comparing to Rust, which has the same constraints (non-escapable references and non-copyable types) to work with. Rust doesn't have properties, but generally provides access to state via fn xxx(&'a self) -> &'a T
(roughly, "borrow" in this vision) and fn xxx_mut(&'a mut self) -> &'a mut T
(roughly, "mutate" in this vision). Combined, these cover the vast majority of use cases — it's very rare in Rust to see other ways of accessing state.
In cases where these two things don't work, Rust generally simply returns another — nonescaping, noncopyable — object, which might (or might not) implement Deref
to allow use as a "smart pointer". This object is then free to include cleanup logic (the tail of the coroutine, in this vision) in its drop
(deinit
). HashMap
access works this way — looking up by a key vends an Entry
, which can be used to access, insert, or replace in that position. It's not a perfectly ergonomic API, but it does reflect the realities of interacting with a map.
I think, if Swift had the concept of first-class non-escaping references (and we're so close — Span
is it, except that it's a range instead of a single element — we wouldn't need more accessors than just "give me an immutable nonescaping reference" and "give me a mutable nonescaping reference". It might be useful/ergonomic to steal Deref
too.
Damage
Currently, if a protocol needs to read a value, it specifies the requirement as get
, or to write, as set
. Obviously, these don't currently work for noncopyable or nonescaping types, which is why we're here! But in a world with six different accessors, what can a protocol safely require? get
/set
are no longer the most general, but borrow
/mutate
won't work in all cases either; nor will yield
/yield inout
. It kind of drives a wedge into the ecosystem, where some packages will (reasonably!) optimize for performance, others will (reasonably!) optimize for maximum compatibility, and still others will (reasonably!) optimize for ease-of-use in common cases, and no amount of "best practice" will cover all cases, or make those packages interoperable.
Even the sheer amount of knowledge about which combinations of noncopyable and nonescapable work with which accessors, which accessors can witness which other accessors, etc. is going to render this difficult for experts, and impenetrable to beginners. And beginners are going to see it, when they command-click through to the Dictionary
subscript to find not set
but yield inout
.
What, then?
I agree, get
/set
in their current forms don't work. But I think that this six-pronged approach throws Swift's baby out with the bathwater. It's an "easy" solution — provide every feature anyone ever wanted — but it doesn't take into account the complexity and divisions that it leaves behind.
In an ideal world, we would
- keep
get
/set
as the only accessor keywords, to avoid dividing the ecosystem - allow noncopyable/nonescaping types to participate — though I'm unconvinced this is strictly necessary. If certain ergonomic APIs are only available to Copyable types, and "weirder" types have to use "weirder" APIs, that's not the end of the world, surely?
- have the compiler perform whatever heavy lifting / accessor style matrixing / etc. is required to have these things interoperate.
I'd at least like to see a discussion of why we can't get any closer to this than the six accessors proposed…
In the other thread I made a proposal that I still think is interesting to add only two of these four (new) accessors: [Pitch] Modify and read accessors - #40 by KeithBauerANZ
But what if we just… didn't add any of these? We're already trying to say that noncopyable and nonescaping are in some way "advanced" features of the language, why should we demand that these be able to be exposed through properties at all, given the complexity of doing so? Would it be the end of the world if Array
looked something like:
subscript(_ index: Int) -> Element where Element: Copyable { ... }
borrowing func borrow(_ index: Int) -> Ref<Element> { ... }
borrowing mutating func mutate(_ index: Int) -> MutableRef<Element> { ... }
Copyable? life as normal. ~Copyable? slightly less ergonomic, but still usable.
or if Dictionary
looked something like
subscript(_ key: Key) -> Value? where Key: Copyable, Value: Copyable { ... }
borrowing func borrow(_ key: borrowing Key) -> Ref<Value>? { ... }
borrowing mutating func entry(_ key: consuming Key) -> Entry { ... }
struct Entry: ~Escapable {
mutating func orInsert(_ value: consuming Value) -> MutableRef<Value> { ... }
mutating func replace(with value: consuming Value)
}
Up to you if Dictionary of Copyables gets to keep using the private coroutine setter, or if it gets deprecated in favor of something somebody outside the stdlib can implement. ~Copyable? Jump through the hoops to make that work.
Or maybe it's all fine
Having spent an hour writing all this, I'm now arguing myself around to "maybe this vision provides this anyway" — everyone should still standardize on get
/set
, these "weird" accessors are only for weird edge-cases anyway, that most code should never need to interact with. The docs for Array and Dictionary can lie to beginners and claim { get set }
if they need to.