Here’s a way to make Matt’s method even more composable, with smaller modules and a clear separation of concerns:
Module `DynamicDispatch`
// Module `DynamicDispatch`
public protocol DynamicallyDispatched {
associatedtype Input
associatedtype Result
func attemptAction() -> Result?
}
public protocol ProxyProtocol { associatedtype Wrapped }
public enum Proxy<Wrapped>: ProxyProtocol {}
Module `DynamicDispatchImplementation`
// Module `DynamicDispatchImplementation`
import DynamicDispatch
public protocol KnownConformance {
static func performAction<T: DynamicallyDispatched>(_ t: T) -> T.Result
}
public protocol ConformanceMarker {
associatedtype A: DynamicallyDispatched
}
extension ConformanceMarker {
public static func attempt(_ a: A) -> A.Result? {
(self as? KnownConformance.Type)?.performAction(a)
}
}
Module `CollectionDispatch`
// Module `CollectionDispatch`
@_exported
import DynamicDispatch
@_implementationOnly
import DynamicDispatchImplementation
public protocol AttemptIfBidirectional: DynamicallyDispatched {
override associatedtype Result // To enable type inference
func action<T: BidirectionalCollection>(_ t: T.Type) -> Result where T == Input
}
extension AttemptIfBidirectional {
public func attemptAction() -> Result? {
BidirectionalMarker.attempt(self)
}
}
private enum BidirectionalMarker<A: AttemptIfBidirectional>: ConformanceMarker {}
extension BidirectionalMarker: KnownConformance where A.Input: BidirectionalCollection {
static func performAction<T: DynamicallyDispatched>(_ t: T) -> T.Result {
(t as! A).action(A.Input.self) as! T.Result
}
}
// ...and similar for `RandomAccess`, etc.
Module `MyCollectionAlgorithms`
@_implementationOnly
import CollectionDispatch
private struct LastIndexIfBidirectional<P: ProxyProtocol>: AttemptIfBidirectional
where P.Wrapped: Collection
{
typealias Input = P.Wrapped
typealias Result = Input.Index?
var x: Input
init<T>(_ x: T) where P == Proxy<T> {
self.x = x
}
func action<T: BidirectionalCollection>(_ t: T.Type) -> Result where T == Input {
return x.isEmpty ? nil : x.index(before: x.endIndex)
}
}
extension Collection {
public var lastIndex: Index? {
if isEmpty { return nil }
if let x = LastIndexIfBidirectional(self).attemptAction() { return x }
var i = startIndex
while true {
let j = i
formIndex(after: &i)
if i == endIndex { return j }
}
}
}
// ...etc.
User code
import MyCollectionAlgorithms
func foo() {
print([1.0, 2.0, 3.0].lastIndex) // Optional(2)
print([4: 5, 6: 7].lastIndex) // Something ridiculously long
print((8...9).lastIndex) // Something moderately long
print("".lastIndex) // nil
}
The first 2 modules are implemented once, with instructions on how to use them.
Then any number of modules like CollectionDispatch
can be written, and they are quite simple. For each set of constraints, you essentially copy-and-paste the code, only changing the names and constraints. Importantly, the function with the force-casts gets pasted verbatim, with no changes at all, not even to its signature.
(I tried to lift the force-casting into one of the earlier modules, but couldn’t find a way.)
Next, any number of modules like MyCollectionAlgorithms
can be written, importing as many modules like CollectionDispatch
as they want. Their authors just need to implement an action type and give it a generic entry-point. They don’t need to know how the dynamic dispatch actually works.
And finally, user code can import modules with dynamically-dispatched algorithms, and call them like normal. From the perspective of these developers, those modules simply provide APIs like any other.