Test if a type conforms to a non-existential protocol

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.

2 Likes