Ah, I see what you're getting at. Yes, not having a common term that covers both mutable and non-mutable "access bindings" is an annoying problem with our current terminology. I don't think we've ever talked about "mutable borrows" in Swift, although IIUC the Rust community does. ("Borrow" is not a term in the Rust language itself, but ref is.)
The point is well taken, although I'm doubtful that any halfway decent name here can totally avoid suggesting other concepts in adjacent contexts. Us humans, though, we are pretty good at context: we should consider whether the adjacency is actively misleading and outweighs other benefits, or if it's largely harmless and not likely to be improved upon.
Here, yes, there are C++ references, and regex references, and ReferenceWritableKeyPaths, and reference types. And, we have the more on-point precedent of Rust's Ref. So, that there'd potentially one Ref in Swift but already many different references, I think, helps assuage the concern (for me) that users will go confidently wrong. (Compare to "pointer", a term we've actively avoided: a pointer is a pointer is a pointer, and this isn't a pointer.)
For me, the clincher is that even the title of this proposal has to explain that the types are "safe, first-class references," and it doesn't seem like folks have been actively misled. So my thinking is: If we're going to explain that it's a Foo, why name it Bar and then have to say that a Bar is a Foo, when we can just...call it Foo?
Having had some time to think on the issue of introducers, given the likely source compatibility issues with a totally new three-letter introducer (or, really, any reasonable word of any length), I'm coming around to a Rust-like double barreled approach here:
If we call the types Ref and MutableRef, bindings can be let ref and var ref, which don't seem too bad. And still fewer letters than borrowing...
If the direction we want to take when we get to bindings it to have a single keyword that changes a let/var to a reference, I think we should flip the order so that ref (or whatever keyword we choose) becomes a modifier of the let/var (and thus preceding it). Since we're going to have to introduce a contextual keyword, we already have precedent for contextual modifiers. It also avoids this awkward situation where both are valid Swift:
let ref x = y // ✅
let ref = y // ✅
flipping it around we get:
ref let x = y // ✅
ref let = y // ❌
This also slides naturally into the existing AST, since most decl nodes support a list of modiifers.
This seemed to be one of the most liked spellings when I suggested something similar a couple of years ago. It was slightly different because I optimized for immutable bindings:
ref x = y // immutable borrow
ref var x = &y // mutable borrow
for ref x in array {}
for ref var x in &array {}
What are the performance guarantees of these types in unspecialized generic code where the Value type isn't known at compile time?
Should they be equivalent to UnsafePointer/UnsafeMutablePointer or is there some overhead expected?
Edit: Seems like accessing .value on Borrow calls into swift_dereferenceBorrow? That would make it not suitable for writing low level code that isn't guaranteed to be specialized.
Edit2: Inout doesn't seem to have this problem and generates the same code as UnsafeMutablePointer.
There is some overhead like you've seen calling into swift_dereferenceBorrow, but this overhead is actually extremely minimal. You can see the implementation of this function here: swift/stdlib/public/runtime/Borrow.cpp at 2a841f605bb482dd339b0556e535ce5623c4437f · swiftlang/swift · GitHub
which compiles down to this on arm64:
_swift_dereferenceBorrow:
ldur x8, [x0, #-0x8]
ldr x9, [x8, #0x40]
cmp x9, #0x20
b.hi loc_2e3df4
ldrh w8, [x8, #0x52]
mov w9, #0x310
tst w8, w9
b.eq loc_2e3df8
loc_2e3df4:
ldr x1, [x1]
loc_2e3df8:
mov x0, x1
ret
Right, Inout is always pointer based unlike Borrow where the referent we're pointing at can either be the value itself or a pointer to the value.
Does this need to be a runtime function? The cost of a function call (especially to another library) is still way higher than a pointer dereference.
The body of _swift_dereferenceBorrow together with the overhead of calling the function (3x number instructions vs UnsafePointer) is quite a lot for just a harmless looking dereference. This can quickly add up being a significant cost.
Do we really have benchmarks that show that this is a negligible cost? While sometimes it is possible to fallback to UnsafePointer or a safe wrapper around it, if Borrow ends up being used in a lot of standard library API, we would end up not being able to use any of it again. Similar to how we can't use any of the Collection API today in unspecialized code, even on UnsafePointer and one needs to write manual implementations because the unspecialized code is just way too slow. Collection is an extreme example because the non-inlined _read accessor allocates per call but it would be nice if the new Collection like APIs would be as efficient as if one would use UnsafeBufferPointer (+ bounds checks in some cases).
Adding a new variable introducer has a potential for source breaks because a tuple pattern can be ambiguous with a call:
ref (foo, bar) // is this calling `ref(_:_:)` or is it declaring `foo` and `bar` as `ref`s?
This is a problem with any possible keyword,[1] but if we choose something that isn't often used as an identifier, we might get away with it in practice. To evaluate that, I built a little SwiftSyntax tool which counts un-backticked uses of identifiers (distinguishing them from keywords) and ran it on the 147 projects in the source compatibility suite. Here's what it found:
Individual projects
borrow |
ref |
inout |
mut |
Project |
|---|---|---|---|---|
| - | - | - | - | ../project_cache/ACHNBrowserUI |
| - | - | - | - | ../project_cache/Alamofire |
| - | - | - | - | ../project_cache/AMScrollingNavbar |
| - | - | - | - | ../project_cache/async-http-client |
| - | - | - | - | ../project_cache/AsyncNinja |
| - | - | - | - | ../project_cache/Base64CoderSwiftUI |
| - | - | - | - | ../project_cache/BeaconKit |
| - | - | - | - | ../project_cache/BlueSocket |
| - | - | - | - | ../project_cache/Bow |
| - | 32 | - | - | ../project_cache/BricBrac |
| - | - | - | - | ../project_cache/CareKit |
| - | - | - | - | ../project_cache/Chatto |
| - | - | - | - | ../project_cache/CleanroomLogger |
| - | - | - | - | ../project_cache/CoreStore |
| - | - | - | - | ../project_cache/cub |
| - | - | - | - | ../project_cache/Deferred |
| - | - | - | - | ../project_cache/DNS |
| - | 8 | - | - | ../project_cache/Doggie |
| - | - | - | - | ../project_cache/Dollar |
| - | - | - | - | ../project_cache/Dwifft |
| - | - | - | - | ../project_cache/Eureka |
| - | - | - | - | ../project_cache/exercism-swift |
| - | - | - | - | ../project_cache/fluent |
| - | - | - | - | ../project_cache/Graphiti |
| - | - | - | - | ../project_cache/GraphQL |
| - | - | - | - | ../project_cache/GRDB.swift |
| - | 4 | - | - | ../project_cache/grpc-swift |
| - | - | - | - | ../project_cache/Guitar |
| - | - | - | - | ../project_cache/Html |
| - | - | - | - | ../project_cache/hummingbird |
| - | - | - | - | ../project_cache/IBAnimatable |
| - | - | - | - | ../project_cache/json-logic-swift |
| - | - | - | - | ../project_cache/JSQCoreDataKit |
| - | - | - | - | ../project_cache/JSQDataSourcesKit |
| - | 5 | - | - | ../project_cache/KeychainAccess |
| - | - | - | - | ../project_cache/Kickstarter-Prelude |
| - | - | - | - | ../project_cache/Kickstarter-ReactiveExtensions |
| - | 6 | - | - | ../project_cache/Kingfisher |
| - | 19 | - | - | ../project_cache/Kitura |
| - | - | - | - | ../project_cache/kommander |
| - | - | - | - | ../project_cache/Kronos |
| - | - | - | - | ../project_cache/Lark |
| - | - | - | - | ../project_cache/launchscreensnapshot |
| - | 2 | - | - | ../project_cache/line-sdk-ios-swift |
| - | - | - | - | ../project_cache/mapper |
| - | 7 | - | - | ../project_cache/ModelAssistant |
| - | - | - | - | ../project_cache/MovieSwift |
| - | - | - | - | ../project_cache/Moya |
| - | - | - | - | ../project_cache/mqtt-nio |
| - | - | - | - | ../project_cache/NetService |
| - | - | - | - | ../project_cache/Nimble |
| - | - | - | - | ../project_cache/NonEmpty |
| - | - | - | - | ../project_cache/NSAttributedStringBuilder |
| - | - | - | - | ../project_cache/ObjectMapper |
| - | - | - | 17 | ../project_cache/Overture |
| - | - | - | - | ../project_cache/package-distributed-system |
| - | 30 | - | - | ../project_cache/penny-bot |
| - | - | - | - | ../project_cache/Perfect |
| - | - | - | - | ../project_cache/PinkyPromise |
| - | 44 | - | - | ../project_cache/Plank |
| - | - | - | - | ../project_cache/ProcedureKit |
| - | 13 | - | - | ../project_cache/PromiseKit |
| - | - | - | - | ../project_cache/R.swift |
| - | - | - | - | ../project_cache/Re-Lax |
| - | - | - | - | ../project_cache/ReactiveCocoa |
| - | - | - | - | ../project_cache/ReactiveKit |
| - | - | - | - | ../project_cache/ReactiveSwift |
| - | - | - | - | ../project_cache/Result |
| - | - | - | - | ../project_cache/ReSwift |
| - | 4 | - | - | ../project_cache/RxDataSources |
| - | - | - | - | ../project_cache/RxReactiveObjC |
| - | 4 | - | - | ../project_cache/RxSwift |
| - | - | - | - | ../project_cache/Serpent |
| - | 6 | - | - | ../project_cache/siesta |
| - | 6 | - | - | ../project_cache/siesta-legacy |
| - | - | - | - | ../project_cache/Smtp |
| - | - | 2 | - | ../project_cache/Sourcery |
| - | - | - | - | ../project_cache/SRP |
| - | - | - | - | ../project_cache/Starscream |
| - | - | - | - | ../project_cache/Surge |
| - | - | - | - | ../project_cache/swift-algorithms |
| - | - | - | - | ../project_cache/swift-argument-parser |
| - | 29 | - | - | ../project_cache/swift-atomics |
| - | - | - | - | ../project_cache/swift-cluster-membership |
| - | 343 | - | - | ../project_cache/swift-collections |
| - | - | - | - | ../project_cache/swift-collections-benchmark |
| - | - | - | - | ../project_cache/swift-crypto |
| - | 788 | - | - | ../project_cache/swift-distributed-actors |
| - | - | - | - | ../project_cache/swift-distributed-tracing |
| - | 240 | - | - | ../project_cache/swift-futures |
| - | - | - | - | ../project_cache/swift-log |
| - | - | - | - | ../project_cache/swift-metrics |
| - | - | - | - | ../project_cache/swift-nio |
| - | - | - | - | ../project_cache/swift-nio-extras |
| - | - | - | - | ../project_cache/swift-nio-http2 |
| - | - | - | - | ../project_cache/swift-nio-ssh |
| - | 62 | - | - | ../project_cache/swift-nio-ssl |
| - | - | - | - | ../project_cache/swift-nio-transport-services |
| - | 22 | - | - | ../project_cache/swift-numerics |
| - | 67 | - | - | ../project_cache/swift-openapi-generator |
| - | - | - | - | ../project_cache/swift-openapi-runtime |
| - | 6 | - | - | ../project_cache/swift-power-assert |
| - | - | - | - | ../project_cache/swift-protobuf-plugin-example |
| - | - | - | - | ../project_cache/swift-sdk-generator |
| - | - | - | - | ../project_cache/swift-standard-library-preview |
| - | - | - | - | ../project_cache/swift-system |
| - | - | 1 | - | ../project_cache/swift-testing |
| - | - | - | - | ../project_cache/SwiftDate |
| - | - | - | - | ../project_cache/SwifterSwift |
| - | - | - | - | ../project_cache/SwiftFormat |
| - | - | - | - | ../project_cache/SwiftGraph |
| - | - | - | - | ../project_cache/SwiftLint |
| - | - | - | - | ../project_cache/SwiftLint-Legacy |
| - | - | - | - | ../project_cache/SwiftyJSON |
| - | - | - | - | ../project_cache/SwiftyStoreKit |
| - | - | - | - | ../project_cache/SyndiKit |
| - | - | - | - | ../project_cache/Tagged |
| - | - | - | - | ../project_cache/Then |
| - | - | - | - | ../project_cache/vapor_apns |
| - | - | - | - | ../project_cache/vapor_async-kit |
| - | - | - | - | ../project_cache/vapor_console-kit |
| - | - | - | - | ../project_cache/vapor_fluent-kit |
| - | - | - | - | ../project_cache/vapor_fluent-mysql-driver |
| - | - | - | - | ../project_cache/vapor_fluent-postgres-driver |
| - | - | - | - | ../project_cache/vapor_fluent-sqlite-driver |
| - | - | - | - | ../project_cache/vapor_jwt |
| - | - | - | - | ../project_cache/vapor_jwt-kit |
| - | - | - | - | ../project_cache/vapor_leaf |
| - | - | - | - | ../project_cache/vapor_leaf-kit |
| - | - | - | - | ../project_cache/vapor_multipart-kit |
| - | - | - | - | ../project_cache/vapor_mysql-kit |
| - | - | - | - | ../project_cache/vapor_mysql-nio |
| - | 12 | - | - | ../project_cache/vapor_postgres-kit |
| - | - | - | - | ../project_cache/vapor_postgres-nio |
| - | - | - | - | ../project_cache/vapor_queues |
| - | - | - | - | ../project_cache/vapor_queues-redis-driver |
| - | - | - | - | ../project_cache/vapor_redis |
| - | - | - | - | ../project_cache/vapor_routing-kit |
| - | - | - | - | ../project_cache/vapor_sql-kit |
| - | - | - | - | ../project_cache/vapor_sqlite-kit |
| - | - | - | - | ../project_cache/vapor_sqlite-nio |
| - | - | - | - | ../project_cache/vapor_template-bare |
| - | - | - | - | ../project_cache/vapor_template-fluent-postgres-leaf |
| - | - | - | - | ../project_cache/vapor_toolbox |
| - | - | - | - | ../project_cache/vapor_vapor |
| - | - | - | - | ../project_cache/vapor_websocket-kit |
| 25 | - | - | - | ../project_cache/violet |
borrow |
ref |
inout |
mut |
Totals |
|---|---|---|---|---|
| 25 | 1759 | 3 | 17 | Use sites |
| 1 | 24 | 2 | 1 | Projects with at least one use site |
Roughly 1 in 6 source compat suite projects uses ref as an identifier; the other keywords we're thinking about are practically never used as identifiers. That doesn't mean every one of these projects has an ambiguous call, but it does seem reasonable to worry that some code out there will.
(This is not a problem with ref as a modifier, since arbitrary identifiers can't be followed by let or var.)
With the odd exception of
inout; it already has to be backticked to be used as a base name. ↩︎
I don’t see reference bindings like ref var doing anything on their own. Borrowing accessors already solve any use case that I can think of except local variables. I think references as a type like Borrow<…> fit better there and also solve the more general problem of using references in generics. I don’t see why these need to be separate features and I don’t think adding more types of variable bindings will help with complexity at all.
I generally like the idea of references as a type, but personally, I would prefer having references be a modifier on the type:
let myRef: ref MyStruct = …
This way, ref MyStruct can be a nonescapable, lifetime-bound view of MyStruct that could still be used in generics. The compiler could then dereference it automatically without any special Deref trait. Mutable references could be something like: &ref MyStruct (although I’m not married to that).
The code is potentially reasonable to inline into callers rather than call out to the runtime for. However, our general assumption is that projects trying to optimize performance to the level of caring about function call overhead (not the cost of the actual function body, but the basic cost of making the call) is going to want to find a way to allow their generic code to be specialized for concrete types.
For technical reasons, Borrow cannot be as simple as passing an address around. This is ultimately forced by the long-ago decision to pass certain borrowed parameters by value rather than by reference; we wouldn’t want to prevent such parameters from being used to initialize a Borrow, or to unnaturally shrink the scope of the resulting Borrow, and potentially force the introduction of a two-tier system of always-by-reference and possibly-by-value borrowed parameters to avoid those limitations. But I think it’s overall a good trade-off anyway because it avoids unnecessary indirection when working with borrowed values of known layout, which is very common. It also happens to allow for some interesting expressive capabilities (not yet enabled for good reason) beyond what e.g. Rust can do, like being able to borrow arbitrary small-and-trivial r-values (such as Ints) without having to specifically dump them into global immutable memory so an address can be passed around.
The implication being that, for a concrete T, we expect the compiler to optimize out these costs for dereferencing Borrow<T>?
Yes. When we know the layout of T, we also know the layout of Borrow<T> and can emit this operation trivially.
Today this is not possible in a lot of cases. Value Witness Tables (VWT), Protocol Witness Tables (PWT) and even VTables for classes don't get specialized today. Any kind of dynamic dispatch will run into unspecialized code in any of these if the type is generic.
Is it really the case that those all will eventually get specialized? I would love that but I have heard before that this is at least a code size concern.
Why can't unspecialized code always use a pointers while specialized code use the optimization to be sometimes inline and sometimes be out of line? Only at the boundary there would be a translation needed.
It is both (1) absolutely correct that Swift sometimes has to run unspecialized code depending on the source code patterns and (2) nonetheless true that, if you want to tightly optimize your code, you should be trying to avoid the source code patterns that require that, because there's simply no way to avoid a lot of overhead when running unspecialized code. It's not that we want to completely give up on performance in unspecialized code, but if you can't afford a few extra function prologues and epilogues, you are definitely just not going to be satisfied without specialization.
You can also use the unofficial @_specialize feature to dynamically get back to specialized code if a function has to get run generically but you can enumerate specific types that it's important to optimize it for.
I did already try to answer that question: because then the lifetime of a borrowed value would depend on which representations it had trafficked through.
This is actually an interesting call out. If @specialize (which is an official feature since recently) would also support specializing based on layout constraints we would get back the performance even in unspecialized (well, then partially specialized) generic code, even without knowing the concrete type.
Is there any way the layout constraint that Borrow uses to decide if it is stored inline or not to express it in the language so @specialize could support it in the future?
I don't like the idea of calling this "Ref" or a "reference" more generally, for two reasons.
The first is that this type doesn't match my intuitive sense of what "reference" means. In my mind, a reference is a piece of data that represents and grants access to some other data. This is not necessarily implemented with a pointer, but there's always some kind of indirection, and the distinction between the thing and a reference to the thing is fundamental to its nature. The types being proposed here are sometimes represented as pointers, but not always, and even when they are, they aren't being used for their indirect nature. The point of a borrow is not necessarily that it's in-place, but that it's reserved until it is eventually returned to its original owner.
The second objection is that "reference type" is already a term in Swift for an unrelated concept. Moreover, it's a term for a very important concept that we've put front and center in the language and have invested a lot of effort into teaching. One of the biggest ideas we ask Swift developers to wrap their heads around is the distinction between value and reference types and how they handle mutation differently. Introducing a type called Ref that's primarily used with value types and imposes value-type exclusivity semantics blurs an image we've spent the last twelve years trying to bring into focus.
Personally, I think Borrow is a perfectly good name for the immutable type. This type is nothing more or less than a borrow reified into a value; Borrow conveys that quite clearly, at the cost of being meaningless if you don't know what a borrow is (but can you actually use this type if you don't know what a borrow is?). MutableBorrow could then be used for the mutable equivalent.
If we're worried about introducers, I'm quite unconvinced that a three-letter introducer is necessary, especially for a fairly advanced feature. var and let are used by every Swift programmer; borrows, mutable and otherwise, will be much more rare. Instead, I would just use lowercase borrow with an & for mutation:
borrow foo = ...
borrow &mutableFoo = ...
Or, if you hate using & like this, use inout for the introducer (where it's closely analogous to inout parameters) but not for the type:
borrow foo = ...
inout mutableFoo = ...
(Yes, it is in theory possible for borrow, like any new introducer, to break source. I don't think it'll be a problem for this specific name in practice; "borrow" doesn't seem to come up often in code and we've been gradually introducing it as a contextual keyword in other contexts for several releases now. If we're reasonably sure we want to do this but that it's not going to make 6.4, we could even deprecate uses now that will become ambiguous later. Or we could do the slow but safe thing: tie it to an upcoming feature and perhaps let people spell it borrow let for now.)
I think an interesting alternative to "borrow" is "access". In programming generally, a variable is said to be "accessed" when it's read or written to. In Swift specifically, the term "formal access"/"access" has been used by the community, in documentation, and in evolution proposals, in a way that I think is basically synonymous with how the Rust community uses the term "borrow". For example, we sometimes speak of a "conflicting access to a variable". It also nicely lines up with the use of "accessor" to refer to get, set, borrow, mutate, etc.
Maybe "access" and "borrow" have subtly different connotations.
I think "borrow" has the connotation of being non-consuming, unlike "access". In the Rust Book, the concept of "borrowing" is introduced first and foremost as a way to use a value without consuming it, as an escape hatch from the limitations of pure move semantics. This could be significant if Swift ever gets consuming references, which Rust lacks but C++ (kind of) has with rvalue references. ConsumingBorrow or OwnedBorrow would seem like an oxymoron, but ConsumingAccess or OwnedAccess could make sense.
Another difference from Rust is that in Swift, an access/borrow can perform extra work. For example, an access/borrow of a computed property with a getter and setter materializes a memory location that didn't previously exist. To me, "access" seems to connote this aspect better than "borrow" does, but this logic could be circular/self-fulfilling if it's just from my experience reading "access" in the context of Swift and "borrow" in the context of Rust.
The concept of "consuming borrow" could make sense if we consider "borrow" to mean "borrowing a memory location" instead of "borrowing a value". A consuming reference "borrows", and is lifetime-bound to, a memory location, even though the value itself can outlive that memory location once it is moved elsewhere. Going even further, since a mutable reference can change the value it points to, or even replace it entirely, it makes more sense to me to speak of "mutably borrowing a memory location" than "mutably borrowing a value". Maybe the terms "access" and "borrow" have different relationships with this distinction. If one "borrows" a memory location, they can be said to "access" both the memory location and the value within. For example, we speak of "conflicting accesses to variables", and variables are one kind of memory location.
Besides a consuming reference type, maybe another future direction to fit into the naming scheme is reference types that capture the state of a yielding accessor coroutine (so that, unlike the reference types currently being proposed, they can outlive the scope they were created in even if they were created from a yielding access). Maybe they could be called YieldedBorrow and YieldedMutableBorrow, YieldedAccess and YieldedMutableAccess, YieldingBorrow and YieldingMutableBorrow, or YieldingAccess and YieldingMutableAccess.
A possible downside of "access" compared to "borrow" is that it could be more jargon-y. "Borrow" is widely known in the Rust community, while I've mostly seen "access" in more technical discussions of Swift. And in Swift, at least "borrow" is currently used in the syntax itself, albeit specifically for shared/immutable access.
Sticking with some form of Borrow here is...fine. If we do so, I'd prefer Borrowed by analogy with Optional, as to me it just sounds more natural to say that you've got "a borrowed foo" rather than "a borrow foo." (I'd stick with MutableBorrowed rather than MutablyBorrowed though; it's fine to say that something is a mutable, borrowed foo.) I did check the dictionary and (excluding an unrelated and very strange definition) it appears there's no real English language precedent for nouning or adjectiving this particular verb.
I must admit, though, I can't subscribe to your two objections to Ref. Indeed, it would seem that they're mutually exclusive: as Swift doesn't promise that it'll never stack promote a value of a reference type, your first concern would apply also to the "very important concept" we already have that's the core of your second concern.
As to the first objection on its own: Semantically, I do see this type as an indirection, and I think we ought to teach it that way, in no small part because I can't see clear to a tidier way of explaining what a Mutable(Ref|Borrow) would be—both the title of this proposal and the analogy that I really like about being a MutableSpanOfOne reinforce the interpretation. That its immutable counterpart may not be implemented with an actual indirection behind the scenes is (a) not a behavior that we'd want users to rely on—as I understand it, whether there's actual indirection or not relies on type layout that isn't itself guaranteed in the surface language and could vary by architecture, etc.; and (b) of a kind with how Swift optimizes notional semantics generally: for instance, values are notionally copied all over the place without actual copies; it's admittedly not great for beginner learning, but by the time that users will be reaching for this type, the idea that there could be notional indirection without actual indirection should be old hat.
As to the second objection: It seems to me actively useful rather than wrongheaded to draw parallels between var x: MutableRef<SomeValueType> and var y: SomeReferenceType. They're clearly not the same thing, but you can also tell at a glance that they're not straining to pretend to be; rather, the semantics rhyme to an extent that parallels how much the terminology echoes each other. I kind of like that a user armed only with the knowledge of how value and reference types differ from each other and who knows that, for example, Int is a value type can start to get some purchase on what a MutableRef<Int> is and might do just by composing that knowledge.
+1 for ref
+1 for Reference
-1 for Ref