Extending type that also conforms to another protocol

I have a type for a Vector, where I implement a version for Double and a version for Float.

protocol Vector {
    associatedtype ScalarType: BinaryFloatingPoint

    var a: ScalarType
    var b: ScalarType
    var c: ScalarType
}

struct Float3: Vector {
    typealias ScalarType = Float

    var a: Float
    var b: Float
    var c: Float
}

struct Double3: Vector { /* . . . */ }

Additional functionality is provided using protocols. For example I have a protocol for computing a dot product that may or may not be implemented by the Vector type.

protocol DotProduct {
    func dot(_ other: Self) -> ScalarType
}

In some other area of the codebase, I implement another type that uses the Vector as an associated type. For example, a triangle...

struct Triangle<T: Vector> {
    var a: T
    var b: T
    var c: T
}

I implement various extensions that are available if the Vector type implements certain protocols. For example, compute the barycentric coordinates if the underlying vector type supports dot product.

extension Triangle: Barycentric where VectorType: DotProduct {
    func barycentricCoordinates(for position: VectorType) -> (u: VectorType.ScalarType, v: VectorType.ScalarType, w: VectorType.ScalarType)?
}

Somewhere else in my code, I implement a function that extends Triangle to add a convenience function that calls the barycentricCoordinates function.

extension Triangle: TestPosition {
    func positionIsInsideTriangle(_ position: VectorType) -> Bool {
        barycentricCoordinates(for: position) != nil
    }
}

Sadly, this hits a compilation error, as TestPosition is dependent on Barycentric conformance which is only exists if the associated vector type conforms to DotProduct.

This can be solved by adding a conformance requirement to the extension:

extension Triangle: TestPosition where VectorType: DotProduct { /* . . . */ }

However, this requires that TestPosition knows enough about the implementation of the Barycentric extension to know its requirements. Additionally, if the requirements for Barycentric conformance are changed, multiple places will need updating. In some cases, this is more than a single conformance.

In reality all I really care about is whether Triangle conforms to Barycentric or not.

I'd hope I could do something like this:

extension Triangle: TestPosition where Triangle: Barycentric { /* . . . */ }

But that also doesn't work.

Am I missing something obvious here? Is there a way to extend a type if it also conforms to another protocol?

Thanks!

Is there any way you can simplify this? Try creating a sample project with the compilation error. It's very hard to follow your problem with isolated snippets of code.

May I ask why you would have instances of vectors that don't support computing the dot product? I would think that all vectors should have that ability implemented.

1 Like

Yes, apologies. Here is a simpler version of the code. It's a bit of a silly example, but I hope it explains the issue I am having.

// A base generic type.
//
struct MyType<T: BinaryFloatingPoint> {
    var baseValue: T
}

// Extend the type to conform to A.
//
protocol A {
    var a: String { get }
}

extension MyType: A where T: CustomStringConvertible  {
    var a: String { baseValue.description }
}

// Extend the type to conform to B.
//
protocol B {
    var b: Int { get }
}

extension MyType: B {
    var b: Int { a.count }
}

Now, this fails to build, as B calls A, but A is dependent on type T conforming to CustomStringConvertible.

This can be resolved by matching the conformance requirements of B to match A.

extension MyType: B where T: CustomStringConvertible {
    var b: Int { a.count }
}

However, this quickly gets out of control. Additionally B doesn't care how A is implemented, it just knows that A exists on the type and is available.

Ideally I could do something like this, where B is saying I want to extend MyType whenever MyType also conforms to A. B knows that it uses A, so this makes sense.

extension MyType: B where MyType: A {
    var b: Int { a.count }
}

or

extension MyType: B where MyType is A {
    var b: Int { a.count }
}

Neither of these work.

I'm wondering if there's some way to approach this problem, allowing me to extend a type with a protocol, only in instances where it already conforms to another protocol?

1 Like

Just a poor example on my part. My Vector type is N dimensional, and has different implementations for different component counts. I tried to simplify a complicated example down but something was lost along the way. A better example would be the cross product, which may not make sense or be trivial to implement outside of three dimensions.

1 Like

I now understand your problem. It looks like you're running into a fundamental limitation in the language. I was surprised that this wasn't possible. I assumed that you could extend a generic type to conform to a protocol contingent on it conforming to another protocol. You'll have to file a feature request.

Here's a working example of custom Vector types (2, 3 and 4) with a method added as an extension for only vectors of count 3.
protocol VectorCount {
  static var value: Int { get }
}
extension VectorCount {
  static var indices: Range<Int> { 0 ..< value }
}
protocol VectorCountGreaterThanOrEqualTo2: VectorCount {}
protocol VectorCountGreaterThanOrEqualTo3: VectorCountGreaterThanOrEqualTo2 {}
protocol VectorCountGreaterThanOrEqualTo4: VectorCountGreaterThanOrEqualTo3 {}

enum VectorCount2: VectorCountGreaterThanOrEqualTo2 {
  static let value = 2
}
enum VectorCount3: VectorCountGreaterThanOrEqualTo3 {
  static let value = 3
}
enum VectorCount4: VectorCountGreaterThanOrEqualTo4 {
  static let value = 4
}

protocol Vector {
  associatedtype Count: VectorCount
  associatedtype Element
  init(from elementForIndex: (Int) -> Element)
}

extension Vector {
  static var elementStride: Int { MemoryLayout<Element>.stride }
  subscript(unchecked index: Int) -> Element {
    get {
      withUnsafeBytes(of: self) {
        $0.load(fromByteOffset: index * Self.elementStride, as: Element.self)
      }
    }
    set {
      withUnsafeMutableBytes(of: &self) {
        $0.storeBytes(of: newValue, toByteOffset: index * Self.elementStride, as: Element.self)
      }
    }
  }
  subscript(_ index: Int) -> Element {
    get {
      precondition(Count.indices.contains(index))
      return self[unchecked: index]
    }
    set {
      precondition(Count.indices.contains(index))
      self[unchecked: index] = newValue
    }
  }
}

// MARK: - Element access via x, y, …

extension Vector where Count: VectorCountGreaterThanOrEqualTo2 {
  var x: Element {
    get { self[0] }
    set { self[0] = newValue }
  }
  var y: Element {
    get { self[1] }
    set { self[1] = newValue }
  }
}

extension Vector where Count: VectorCountGreaterThanOrEqualTo3 {
  var z: Element {
    get { self[2] }
    set { self[2] = newValue }
  }
}

extension Vector where Count: VectorCountGreaterThanOrEqualTo4 {
  var w: Element {
    get { self[3] }
    set { self[3] = newValue }
  }
}

// MARK: - Elementwise inits

extension Vector where Count == VectorCount2 {
  init(_ x: Element, _ y: Element) {
    self.init { [x, y][$0] } // (This compile down to a static table lookup.)
  }
}
extension Vector where Count == VectorCount3 {
  init(_ x: Element, _ y: Element, _ z: Element) {
    self.init { [x, y, z][$0] }
  }
}
extension Vector where Count == VectorCount4 {
  init(_ x: Element, _ y: Element, _ z: Element, _ w: Element) {
    self.init { [x, y, z, w][$0] }
  }
}



// MARK: - Concrete vector types:

struct V2<Element>: Vector {
  typealias Count = VectorCount2
  var elements: (Element, Element)
  init(from elementForIndex: (Int) -> Element) {
    elements = (elementForIndex(0), elementForIndex(1))
  }
}

struct V3<Element>: Vector {
  typealias Count = VectorCount3
  var elements: (Element, Element, Element)
  init(from elementForIndex: (Int) -> Element) {
    elements = (elementForIndex(0), elementForIndex(1), elementForIndex(2))
  }
}

struct V4<Element>: Vector {
  typealias Count = VectorCount4
  var elements: (Element, Element, Element, Element)
  init(from elementForIndex: (Int) -> Element) {
    elements = (elementForIndex(0), elementForIndex(1), elementForIndex(2), elementForIndex(3))
  }
}

// MARK: - Adding functionality to vectors of all counts but with certain elements:

extension Vector where Element: AdditiveArithmetic {
  static func +(lhs: Self, rhs: Self) -> Self { Self { lhs[$0] + rhs[$0] } }
}

// MARK: - Adding a method to vectors of a specific count:

extension Vector where Count == VectorCount3 {
  func someMethodOnlyOnVectorsWithCount3() {
    print("hello")
  }
}


func test() {
  var a = V3<Float>(1, 2, 3)
  let b = V3<Float>(1, 2, 3)
  a.x = 0.1
  a.y = 0.2
  a.z = 0.3
  print(a.x)
  print(a.y)
  print(a.z)
  print(a + b)
}

test()

But maybe you can just use SIMD vectors instead?

Yeah, this seems generally useful. I think I'd prefer the spelling where Self: Barycentric rather than where Triangle: Barycentric, but that's not as important as the general feature. I don't know how hard it would be to add to the language, though.

EDIT: I suspect part of the reason it's not supported is because if you didn't have a conformance to Barycentric, it might read like you were saying "if someone else adds this conformance in another module". But the compiler could require the conformance to both exist and be conditional.

Terms of Service

Privacy Policy

Cookie Policy