Swift 4.1 makes generic capabilities compose
Conditional conformance lets containers acquire protocols from their contents. Swift 4.1 uses it to fix nested equality and collapse collection wrappers, with consequences for both APIs and generated libraries.
An array of integers could support equality in Swift 4.0 while an array of optional integers failed to compare. The individual operations looked sufficient. Arrays had an equality operator for equatable elements, and optionals had one for equatable wrapped values. The missing relationship was a protocol conformance connecting those operations to generic code.
Swift 4.1 closes that gap through conditional conformance. The March 29 release announcement includes SE-0143 among its implemented generics proposals. A generic type can now conform to a protocol when its arguments satisfy the necessary constraints. That lets the compiler express a fact about the constructed type, instead of merely providing a specially constrained operation.
Ben Cohen's January explanation of conditional conformance in the standard library shows why this matters beyond convenient equality syntax. The same mechanism allows one collection wrapper to acquire several independent capabilities. The standard library can replace a family of wrappers with one generic type whose conformances describe what its base supports.
I like Swift's standard collections because their value semantics make ordinary mutation easier to reason about. Conditional conformance improves another part of their consistency. A collection should be able to participate in a generic operation when its contents provide the required behavior, and that relationship should continue to work when containers nest.
An operator was not a conformance
Before this release, the standard library could define an equality operator on arrays whose elements conformed to Equatable. That declaration made [1, 2] == [1, 2] work. It did not declare that the array type itself conformed to Equatable. The distinction becomes visible as soon as another generic operation asks for that conformance.
Optional had the same problem. Its constrained equality operator could compare two optional integers, but the optional type did not acquire Equatable conformance merely because the operator existed. An array equality implementation requiring equatable elements therefore could not use an optional integer as its element type. The operators did not compose through the protocol system.
Swift 4.1 gives the library the missing conditional declarations. Array can conform when its element is equatable, and Optional can conform when its wrapped type is equatable. Applied together, these rules establish that an array of optional integers is equatable. Each container contributes its own rule, so the compiler can reason through the nesting.
The following example adapts the standard library article's conversion of strings into optional integers. An unsuccessful integer conversion leaves an absent value in the array.
let parsed = ["1", "2", "x"].map(Int.init)
let expected: [Int?] = [1, 2, nil]
print(parsed == expected)
print(parsed.contains(nil))
Both expressions print true with the behavior described for Swift 4.1. Equality compares the nested structure, while contains uses the element's conformance. That second operation is important because it demonstrates the value of the protocol relationship independently of the array equality operator.
A constrained extension adding a method and a conditional conformance solve different problems. The former makes a particular operation available under a constraint. The latter establishes that a type meets a protocol's requirements under a constraint. Generic consumers can use the latter fact without knowing which container supplied it.
The contents determine the container's promise
The rule does not make every possible instantiation equatable. A container with unsuitable contents still lacks the conformance. That limitation is necessary because the container cannot manufacture a meaningful element comparison that its element type does not provide. The condition records exactly where its implementation gets the required behavior.
The January article documents conditional Equatable and Hashable conformances for Optional, Array and Dictionary when the relevant wrapped values or elements meet the protocols. A capability can therefore propagate through ordinary combinations of these types. Library users gain the composition, while the library retains a precise condition on when it is valid.
The same approach changes failure behavior for Codable. Cohen explains that trying to encode an array of non-codable types now produces a compile-time error instead of the former runtime trap. The useful improvement is the stage at which the program's unmet requirement becomes visible. A generic container's encoding support can depend on the encoding support of its contents.
This makes the type declaration more informative. A generic algorithm can require a protocol and rely on the container's conditional conformance to establish it. The alternative of discovering an unsupported element during execution leaves a fact about the type until later than necessary. Swift 4.1 can express that fact directly.
There is a design discipline here that I value. An abstraction becomes more useful when it preserves the constraints of the values it contains. Conditional conformance gives library authors a way to state those constraints without asking application code to repeat the container's implementation.
Collection wrappers have independent capabilities
Cohen develops a lazy splitting collection to demonstrate the larger design consequence. The sketch holds a base collection and a closure identifying separator elements. Its basic collection implementation can walk forward, locate a separator and return a subsequence. Splitting happens as the wrapper's elements are accessed.
The article explicitly presents this as a sketch, not a newly shipped standard library lazy-split API. That qualification matters when interpreting the release. The example demonstrates what library authors can build with conditional conformance. It also omits behavior such as coalescing empty subsequences, which a complete splitting API would need to decide.
A forward collection does not automatically support backward traversal. If the base supports BidirectionalCollection, however, the wrapper can implement movement to the previous split. Conditional conformance allows that additional protocol to depend on the base's bidirectional capability. The original wrapper remains the same generic type.
Without that facility, the Swift 4.0 approach described in the article needs another wrapper type for the bidirectional case and an overload that selects it. This becomes awkward when a second independent property enters the design. Supporting mutation is a separate question from supporting backward traversal. A collection may provide one without the other.
Creating a type for each supported combination duplicates the structural distinctions in the collection hierarchy. Conditional conformance lets one wrapper acquire each protocol under its own appropriate conditions. Its implementation can keep the shared storage and basic operations together, with additional operations attached to the capabilities that justify them.
This also clarifies the relationship between laziness and traversal. A lazy transformation postpones work until access. Bidirectional conformance describes where indices can move. The two properties can coexist, but one does not establish the other. A wrapper needs to preserve each property through a valid implementation.
Slice shows the reduction in library complexity
The standard library's Slice is the concrete application of this design. Cohen reports that the earlier library needed a dozen implementations, including the combined mutable, range-replaceable and random-access case. Conditional conformance allows one Slice type with four conditional conformances.
The reported consequence is a five percent reduction in the standard library's binary size from this change alone. That figure concerns the library implementation described in the January article. It does not predict that an application using slices becomes five percent smaller. The significant point is that a more expressive generic model removes repeated implementation types.
This is an unusual release improvement because source design and distribution size benefit from the same change. A wrapper no longer needs a separate nominal type for every supported combination. Generic code can observe the protocols that actually hold for its base. Removing those repeated wrappers also removes their implementation cost in the library.
The release announcement connects this work to ABI preparation by listing the collapse of collection wrappers among completed tasks. That should be read as progress toward stabilization. Swift 4.1 does not claim a stable ABI in the announcement. It describes work undertaken as part of the effort targeting Swift 5.
The release adds supporting generic changes
Conditional conformance arrives alongside synthesized Equatable and Hashable conformance, recursive associated-type constraints and hashable standard library indices. These are distinct proposals in the release announcement. Together they make it easier for types and algorithms to state and obtain the protocol relationships they need, although each change has its own rules.
Swift 4.1 also introduces Sequence.compactMap, and removes IndexDistance from Collection. Those changes should not be confused with conditional conformance itself. A release can improve individual collection operations while separately improving the language mechanism that determines which protocols a constructed type supports.
The release is source compatible with Swift 4.0 according to the announcement, and ships with Xcode 9.3 on Apple's platforms. Official Linux binaries cover the Ubuntu versions named there. Package Manager work addresses graphs using different URL schemes and improves performance for graphs with shared dependencies.
For library authors, the substantive new possibility is to revisit wrappers that differ mainly in inherited capabilities. The Slice change provides a specific example of what can be consolidated. A wrapper can retain one representation while declaring bidirectional or mutable behavior only under the conditions that make its implementation valid.