Adding an alias/shorthand for a set of constraints

I'd like to continue a discussion from here:

I've always wanted this feature and as I just discovered the same workaround that @Karl mentioned in this other related thread, I figured it would be interesting to see if and how this feature could be added to the language


Though it probably doesn't add anything substantial to what has already been said in linked threads, I'll reintroduce the requested feature here via an example of a situation where I really wanted it.

Let's say I have this:

protocol VectorIndex {
  // ...
}

protocol Vector: ExpressibleByArrayLiteral, CustomStringConvertible {
  associatedtype Element
  associatedtype Index: VectorIndex
  subscript(index: Index) -> Element { get set }
  init(_ elementForIndex: (Index) -> Element)
}

// ...
( More complete VectorIndex )
// FIXME: Make this inherit/refine CaseIterable once SR-12980 is fixed.
protocol VectorIndex {
  static var count: Int { get }
  var intValue: Int { get }
  var wrappedNext: Self { get }
  var wrappedPrevious: Self { get }
  static var allCases: [Self] { get } // FIXME: Remove once SR-12980 is fixed.
}

enum VectorIndex1: VectorIndex {
  static let count: Int = 1
  case i0
  var intValue: Int {
    switch self {
    case .i0: return 0
    }
  }
  var wrappedNext: Self {
    switch self {
    case .i0: return .i0
    }
  }
  var wrappedPrevious: Self {
    switch self {
    case .i0: return .i0
    }
  }
  // FIXME: Remove once SR-12980 is fixed:
  static var allCases: [Self] { [.i0] }
}


enum VectorIndex2: VectorIndex {
  static let count: Int = 2
  case i0, i1
  var intValue: Int {
    switch self {
    case .i0: return 0
    case .i1: return 1
    }
  }
  var wrappedNext: Self {
    switch self {
    case .i0: return .i1
    case .i1: return .i0
    }
  }
  var wrappedPrevious: Self {
    switch self {
    case .i0: return .i1
    case .i1: return .i0
    }
  }
  // FIXME: Remove once SR-12980 is fixed:
  static var allCases: [Self] { [.i0, .i1] }
}

enum VectorIndex3: VectorIndex {
  static let count: Int = 3
  case i0, i1, i2
  var intValue: Int {
    switch self {
    case .i0: return 0
    case .i1: return 1
    case .i2: return 2
    }
  }
  var wrappedNext: Self {
    switch self {
    case .i0: return .i1
    case .i1: return .i2
    case .i2: return .i0
    }
  }
  var wrappedPrevious: Self {
    switch self {
    case .i0: return .i2
    case .i1: return .i0
    case .i2: return .i1
    }
  }
  // FIXME: Remove once SR-12980 is fixed:
  static var allCases: [Self] { [.i0, .i1, .i2] }
}

enum VectorIndex4: VectorIndex {
  static let count: Int = 4
  case i0, i1, i2, i3
  var intValue: Int {
    switch self {
    case .i0: return 0
    case .i1: return 1
    case .i2: return 2
    case .i3: return 3
    }
  }
  var wrappedNext: Self {
    switch self {
    case .i0: return .i1
    case .i1: return .i2
    case .i2: return .i3
    case .i3: return .i0
    }
  }
  var wrappedPrevious: Self {
    switch self {
    case .i0: return .i3
    case .i1: return .i0
    case .i2: return .i1
    case .i3: return .i2
    }
  }
  // FIXME: Remove once SR-12980 is fixed:
  static var allCases: [Self] { [.i0, .i1, .i2, .i3] }
}
( More complete Vector )
protocol Vector: ExpressibleByArrayLiteral, CustomStringConvertible {
  associatedtype Element
  associatedtype Index: VectorIndex
  subscript(index: Index) -> Element { get set }
  init(_ elementForIndex: (Index) -> Element)
}


// MARK: CustomStringConvertible

extension Vector {
  var description: String {
    return "[" +
      Index.allCases
        .map({ String(describing: self[$0]) }).joined(separator: ", ")
      + "]"
  }
}


// MARK: Vector Initialization


extension Vector {
  init(arrayLiteral elements: Element...) {
    // TODO: Check if this adds overhead in some (maybe even all?) scenarios.
    precondition(elements.count == Index.count)
    self.init { elements[$0.intValue] }
  }
}

extension Vector where Index == VectorIndex2 {
  init(_ e0: Element, _ e1: Element) {
    self.init {
      switch $0 {
      case .i0: return e0
      case .i1: return e1
      }
    }
  }
}
extension Vector where Index == VectorIndex3 {
  init(_ e0: Element, _ e1: Element, _ e2: Element) {
    self.init {
      switch $0 {
      case .i0: return e0
      case .i1: return e1
      case .i2: return e2
      }
    }
  }
}
extension Vector where Index == VectorIndex4 {
  init(_ e0: Element, _ e1: Element, _ e2: Element, _ e3: Element) {
    self.init {
      switch $0 {
      case .i0: return e0
      case .i1: return e1
      case .i2: return e2
      case .i3: return e3
      }
    }
  }
}



// MARK: - Vector Arithmetic


extension Vector where Element: AdditiveArithmetic {
  static func +(lhs: Self, rhs: Self) -> Self { Self { lhs[$0] + rhs[$0] } }
  static func +(lhs: Self, rhs: Element) -> Self { Self { lhs[$0] + rhs } }
  static func +(lhs: Element, rhs: Self) -> Self { Self { lhs + rhs[$0] } }
  static func +=(lhs: inout Self, rhs: Self) {
    for i in Index.allCases { lhs[i] += rhs[i] }
  }
  static func +=(lhs: inout Self, rhs: Element) {
    for i in Index.allCases { lhs[i] += rhs }
  }
  static func -(lhs: Self, rhs: Self) -> Self { Self { lhs[$0] - rhs[$0] } }
  static func -(lhs: Self, rhs: Element) -> Self { Self { lhs[$0] - rhs } }
  static func -(lhs: Element, rhs: Self) -> Self { Self { lhs - rhs[$0] } }
  static func -=(lhs: inout Self, rhs: Self) {
    for i in Index.allCases { lhs[i] -= rhs[i] }
  }
  static func -=(lhs: inout Self, rhs: Element) {
    for i in Index.allCases { lhs[i] -= rhs }
  }
}

extension Vector where Element: Numeric {
  static func *(lhs: Self, rhs: Self) -> Self { Self { lhs[$0] * rhs[$0] } }
  static func *(lhs: Self, rhs: Element) -> Self { Self { lhs[$0] * rhs } }
  static func *(lhs: Element, rhs: Self) -> Self { Self { lhs * rhs[$0] } }
  static func *=(lhs: inout Self, rhs: Self) {
    for i in Index.allCases { lhs[i] *= rhs[i] }
  }
  static func *=(lhs: inout Self, rhs: Element) {
    for i in Index.allCases { lhs[i] *= rhs }
  }
}

extension Vector where Element: FixedWidthInteger {
  static func &+(lhs: Self, rhs: Self) -> Self { Self { lhs[$0] &+ rhs[$0] } }
  static func &+=(lhs: inout Self, rhs: Self) {
    for i in Index.allCases { lhs[i] &+= rhs[i] }
  }
  static func &-(lhs: Self, rhs: Self) -> Self { Self { lhs[$0] &- rhs[$0] } }
  static func &-=(lhs: inout Self, rhs: Self) {
    for i in Index.allCases { lhs[i] &-= rhs[i] }
  }
  static func &*(lhs: Self, rhs: Self) -> Self { Self { lhs[$0] &* rhs[$0] } }
  static func &*=(lhs: inout Self, rhs: Self) {
    for i in Index.allCases { lhs[i] &*= rhs[i] }
  }
  static func /(lhs: Self, rhs: Self) -> Self { Self { lhs[$0] / rhs[$0] } }
  static func /(lhs: Self, rhs: Element) -> Self { Self { lhs[$0] / rhs } }
  static func /=(lhs: inout Self, rhs: Self) {
    for i in Index.allCases { lhs[i] /= rhs[i] }
  }
  static func /=(lhs: inout Self, rhs: Element) {
    for i in Index.allCases { lhs[i] /= rhs }
  }
}

extension Vector where Element: BinaryFloatingPoint {
  static func /(lhs: Self, rhs: Self) -> Self { Self { lhs[$0] / rhs[$0] } }
  static func /(lhs: Self, rhs: Element) -> Self { Self { lhs[$0] / rhs } }
  static func /=(lhs: inout Self, rhs: Self) {
    for i in Index.allCases { lhs[i] /= rhs[i] }
  }
  static func /=(lhs: inout Self, rhs: Element) {
    for i in Index.allCases { lhs[i] /= rhs }
  }
}



// MARK: - Inner Products


infix operator .*: MultiplicationPrecedence
extension Vector where Element: Numeric {

  /// Returns the scalar product, or dot product (the common special case of
  /// the inner product).
  static func .*(lhs: Self, rhs: Self) -> Element {
    var r: Element = 0
    for i in Index.allCases { r += lhs[i] * rhs[i] }
    return r
  }

}

// ...

Since a square matrix can be represented as eg VecN<VecN<Float>> (where VecN<Element>: Vector), I can write generic code for eg computing the determinant of any NxN matrix via an extension like this:

extension Vector where
  Element: Vector,
  Index == Element.Index,
  Element.Element: Numeric
{
  func determinant() -> Element.Element { 
    fatalError("unimplemented")
  }
}

(Note: This is about the ability to express abstractions and do generic programming in Swift. Whether or not I should rather use SIMD or BLAS etc for code like this is irrelevant for the purpose of this discussion.)

Let's say I want to implement a special method of computing the determinant for the specific case of 4x4 matrices (because it is faster than the general method above). I can do this like so:

extension Vector where
  Element: Vector,          // -.
  Index == Element.Index,   //  :-- Note: These again ...
  Element.Element: Numeric, // -'
  Index == VectorIndex4
{
  func determinant() -> Element.Element { 
    fatalError("unimplemented (faster 4x4-specific)")
  }
}

Now, using @Karl's workaround we can (today) factor out the repeated set of constraints from those two extensions like this:

typealias IsSquareMatrix<V> = V where
  V: Vector,
  V.Element: Vector,
  V.Element.Index == V.Index,
  V.Element.Element: Numeric

extension Vector where IsSquareMatrix<Self>: Any {
  func determinant() -> Element.Element { 
    fatalError("unimplemented (NxN)")
  }
}

extension Vector where IsSquareMatrix<Self>: Any, Index == VectorIndex4 {
  func determinant() -> Element.Element { 
    fatalError("unimplemented (faster 4x4-specific)")
  }
}

The requested feature, if/when correctly worked out, should IMO be able to do what this workaround does here, only in a much nicer way of course. I haven't managed to come up with any fantastic concrete suggestions for it's design though.

Sorry for the long post but I felt that it was important to note that IMO the feature should be able to handle scenarios like above (ie both of those extensions), as some of the previous examples/sketches I've seen would not.

Thoughts?

5 Likes

I think It Would Be Nice(TM).

I also think that the design issues are tricky enough, the existing workaround at least functional enough, and the outstanding issues with the existing extension syntax at least numerous enough (e.g., the issue of whether we can eventually give more clarity by having extension some Protocol and by analogy also extensions of the existential type extension any Protocol; the question about whether we can design a syntax to refer to Protocol.SomeAssociatedType using a shorthand in the where clause such as the previously suggested .SomeAssociatedType; the question of whether it is natural to use angle brackets as a synonym for where clauses or whether it misleadingly suggests that we have generic protocols, which we do not have and which are too often confused with other desired features; etc., etc., etc.) that this particular shorthand could and should be on the backburner until we've solved the related issues over time. IMO, at some point, our design for protocols and extensions of their APIs will evolve to a point where a natural shorthand will just fall out.

1 Like

I look forward to this future.

I guess it's at least a couple of years until then though, so in the mean time I guess we'll have to choose between these two options:

  1. Adopting the workaround as a useful way to factor code and increase readability (even though it looks strange, a bit less so once you get used to it I guess).

  2. Continue copy-pasting and trying to avoid mistakes when editing duplicated sets of constraints.

The only reason I can see to choose 2 is because the workaround has some issues which I'm not yet aware of.

I'm amazed you and Karl managed to reinvent SFINAE in Swift. Or to discover that the compiler team had reinvented SFINAE in Swift.

(I don't have much useful to contribute. I agree this idea can be useful, and for better or worse, the fact that it can be expressed today using typealias does encourage coming to a consensus on an actual design sooner rather than later. Or at least promising not to break this.)

4 Likes