I can see the argument that an atomic type is, in some sense, a fundamentally different kind of type from a struct
or a class
. It is not a reference type: first off, it is of great practical important to clients that it is stored inline and does not require an additional allocation; additionally, while its presence in memory is important, it can also be relocated just like any other non-copyable type; also it really could be meaningfully copyable, we just want that to be more explicit (for good reason). But at the same time, it doesn't follow the most important rule of a value type, which is that mutations of / exclusive access to component values always requires exclusive access to the containing value. (This is perhaps clearer with Locked<T>
types, which generally have the same properties as Atomic<T>
.)
This ability to do mutations of the component value without exclusive access to the containing value is what makes atomic types weird, but it's also their whole reason to exist. Swift's exclusivity rules are designed to allow normal accesses to value components to be safe and race-free. That logic works quite well in reverse: once you're enforcing the exclusivity rules, there's no point in using atomic accesses to value components, because there can't possibly be a racing thread anyway. Doing atomic operations on memory you have exclusive access to is just wasting resources in the memory subsystem. Atomic operations exist so that you can achieve some things safely without exclusive access to that memory.
So we've got these types that in some sense are fundamentally different from other types. It's fair to ask whether they ought to look different, either in declaration or in use. The declaration side is out of scope for this review: Atomic<T>
is a single standard library type, and the LSG has agreed that we'll just treat it as a magical one for now. So in this proposal, we're just talking about how it feels to use an atomic type, e.g. to declare an atomic variable. In the future, we'll probably have some way to declare other atomic-like types which (as the name suggests) will probably behave just like Atomic<T>
. The rules we pick here will probably be the rules for those other types, too.
Unfortunately, if we want to use special-case syntax for uses of atomic types, I think we have an unavoidable conflict here with the existing syntax for ownership.
Ownership in Swift is all about establishing either exclusive or shared access to memory. As Joe mentioned upthread, the most important reason, by far, that we might need exclusive access to memory is that it's ordinary memory and we want to mutate it. As a result, all of the keywords we've chosen for exclusive ownership are associated with mutation: mutating
, inout
, var
, etc.
Notably, we use those mutation-oriented keywords even in generic code. Generic code can't directly mutate the value components of opaque values, because the basic rules for direct, concrete accesses depend on knowing what kind of value you're working with. Instead, generic code just enforces exclusive and non-exclusive accesses for abstract operations on opaque values, with no idea about whether those operations actually do any mutation.
For example, suppose you have the type struct Point2i { var x, y: Int }
. If you assign p.y = 5
on a concrete variable of this type, Swift knows that you're assigning into a stored struct
property, and it knows that's a mutation, and it knows specifically how to best enforce exclusivity for p
for that operation. If you're in generic code, and there's a type parameter T
that conforms to protocol PointLike { var x, y: Int { get set } }
, the statement t.y = 5
is just invoking some abstract operation which may or may not do any mutation, and all we know about it is that it happens to require exclusive access to t
. Nonetheless, it's reasonable to still talk about it as if it does mutation, as well as to use the same keywords as we do for operations that definitely do mutation.
Of course, Swift doesn't yet have generics over non-copyable types like Atomic<T>
. We could choose to use rules that accommodate the possibility of non-copyable type parameters being atomic types, e.g. by using different, less mutation-centric keywords in such code. However, I think that would be a poor choice. It'd be even worse to put atomic types into an even-more-refined generic subset, making them not just normal non-copyable types, just so we can enforce the use of these special keywords. In both cases, we're just creating complexity to solve a non-problem, because the generic code doesn't need to care whether it's working with an atomic type. The basic concept I laid out above is good enough: the code is just forwarding opaque values around to abstract operations that it doesn't understand, locally enforcing exclusivity according to the declared requirements of the operations. It doesn't actually matter to map
whether it's passing a borrowed Int
to a function that'll print
it or a borrowed Atomic<Int>
to a function that'll do atomic operations there.
So if we want to impose different spellings on uses of atomic types, we need to understand that we're necessarily engaged in a line-drawing exercise. Someone who wants to understand how atomic types work under generics will have to internalize what Joe is saying about how normal mutation requires exclusive access but atomic mutation does not. The question is just whether it's a good idea to use special-case spellings in code that's concretely working with atomic types. The advantage of doing so is that it will probably better fit most people's intuition about mutability in concrete atomic code ā and it's fair to guess that the preponderance of code working with atomics will be working with Atomic<T>
concretely. The disadvantage is that it adds complexity and might undermine people's ability to make that broader conceptual leap about ownership.