orphan: |
---|
Warning
This proposal was rejected. We ultimately decided to keep Array as a dual-representation struct.
I think that attempting to bridge NSArray
to a concrete type like
Array<T>
will be a poor compromise, losing both the flexibility of NSArray's
polymorphism and the performance afforded by Array<T>
's simplicity.
Here's what I propose instead:
- Rename our current
Array
back toVector
or perhaps something likeContiguousArray
. - Redefine
Array
as a refinement of theCollection
protocol that has integer indices. - Implement an
ArrayOf<T>
generic container, likeAnyIterator
andAnySequence
, that can hold an arbitrary type conforming toArray
. - Bridge
NSArray
from ObjC to SwiftArrayOf<AnyObject>
with value semantics. - Bridge
Array
-conforming types with class element types in Swift to ObjC asNSArray
.
Although I'll be talking about arrays in this proposal, I think the same
approach would work for NSDictionary
and NSSet
as well, mapping them
to generic containers for associative map and unordered container protocols
respectively.
Despite their similar names, NSArray
and Swift's Array
have
fundamentally incompatible design goals. As the root of a class cluster,
NSArray
provides abstraction over many underlying data structures, trading
weaker algorithmic guarantees for better representational flexibility and
implementation encapsulation. Swift's Array
, on the other hand, is intended to be a direct
representation of a contiguous region of memory, more like a C array or C++'s
vector
, minimizing abstraction in order to provide tight algorithmic
guarantees. Many NSArray
implementations are lazy,
such as those over KVO properties or Core Data aggregates, and
transforming them to concrete Array
s would have unintended semantic
effects. And on the other side, the overhead of having to accommodate an
arbitrary NSArray
implementation inside Array
destroys Array
as a simple, high-performance container. Attempting to bridge these two types
will result in an unattractive compromise to both sides, weakening the
algorithmic guarantees of Array while forgoing the full flexibility of
NSArray
.
Swift's answer to container polymorphism is its generics system. The
Collection
protocol provides a common interface to indexable containers
that can be used generically, which is exactly what NSArray
provides in
Cocoa for integer-indexable container implementations. Array
could be
described as a refinement of Collection
with integer indices:
protocol Array : Collection { where IndexType == Int } protocol MutableArray : MutableCollection { where IndexType == Int }
The familiar NSArray
API can then be exposed using default implementations
in the Array
protocol, or perhaps even on the more abstract Collection
and Sequence
protocols, and we can bridge NSArray
in a way that plays
nicely with generic containers.
This naming scheme would of course require us to rename the concrete
Array<T>
container yet again. Vector
is an obvious candidate, albeit
one with a C++-ish bent. Something more descriptive like ContiguousArray
might feel more Cocoa-ish.
Although the language as implemented does not yet support protocol types for
protocols with associated types, DaveA devised a technique for implementing
types that provide the same effect in the library, such as his AnyIterator<T>
and AnySequence<T>
containers for arbitrary Stream
and Sequence
types. This technique can be extended to the Array
protocol, using class
inheritance to hide the concrete implementing type behind an abstract base:
// Abstract base class that forwards the Array protocol class ArrayOfImplBase<T> { var startIndex: Int { fatal() } var endIndex: Int { fatal() } func __getitem__(_ i: Int) -> T { fatal() } // For COW func _clone() -> Self { fatal() } } // Concrete derived class containing a specific Array implementation class ArrayOfImpl<T, ArrayT: Array where ArrayT.Element == T> : ArrayOfImplBase<T> { var value: ArrayT var startIndex: Int { return value.startIndex } var endIndex: Int { return value.endIndex } func __getitem__(_ i: Int) -> T { return __getitem__(i) } // For COW func _clone() -> Self { return self(value) } } // Wrapper type that uses the base class to erase the concrete type of // an Array struct ArrayOf<T> : Array { var value: ArrayOfImplBase<T> var startIndex: Int { return value.startIndex } var endIndex: Int { return value.endIndex } func __getitem__(_ i: Int) -> T { return value.__getitem__(i) } init<ArrayT : Array where ArrayT.Element == T>(arr: ArrayT) { value = ArrayOfImpl<T, ArrayT>(arr) } }
The mutable variant can use COW optimization to preserve value semantics:
struct MutableArrayOf<T> : MutableArray { /* ...other forwarding methods... */ func __setitem__(_ i: Int, x: T) { if !isUniquelyReferenced(value) { value = value._clone() } value.__setitem__(i, x) } }
We could simply make NSArray
conform to Array
, which would be
sufficient to allow it to be stored in an ArrayOf<AnyObject>
container.
However, a good experience for NSArray
still requires special-case
behavior. In particular, NSArray
in Cocoa is considered a value class,
and best practice dictates that it be defensively copy
-ed when used. In
Swift, we should give bridged NSArrays COW value semantics by default, like
NSString
. One way to handle this is by adding a case to the ArrayOf
implementation, allowing it to either contain a generic value or an NSArray
with COW semantics.
We could have an implicit conversion to NSArray
from an arbitrary type
conforming to Array
with a class element type, allowing ObjC APIs to work
naturally with generic Swift containers. Assuming we had support for
conversion_to
functions, it could look like this:
class NSArrayOf<ArrayT: Array where ArrayT.Element : class> : NSArray { /* ...implement NSArray methods... */ } extension NSArray { @conversion_to func __conversion_to< ArrayT: Array where ArrayT.Element : class >(arr: ArrayT) -> NSArray { return NSArrayOf<ArrayT>(arr) } }
NSArray
has reference semantics in ObjC, which is a mismatch with
Swift's value semantics, but because NSArray
is a value class, this is
probably not a problem in practice, because it will be copy
-ed as
necessary as a best practice. There also needs to be a special case for bridging
an ArrayOf<T>
that contains an NSArray
; such a container should be
bridged directly back to the underlying unchanged NSArray
.