Constraining associatedtype of non-generic typealias

I just encountered a situation where it seems impossible to make a typealias . The idea is to have a convenient short name for a protocol with certain constraints on its associatedtypes. For example, one might want:

typealias Randomizable = BinaryFloatingPoint where RawSignificand: FixedWidthInteger

However, when I try to write that, it fails to compile:

error: 'where' clause cannot be attached to a non-generic declaration

Am I missing something, or is this really not supported? If it isn’t, would we need an evolution proposal or is it considered an obvious hole / bug?

3 Likes

We don't have "constraint aliases" in the language today (it's "really not supported" AFAIK) - but I'll let you in on a little secret I discovered. This is really ugly, but believe it or not, it works:

typealias IsFloatColl<T> = T where T: Collection, T.Element: Strideable, T.Element.Stride: BinaryFloatingPoint

extension Collection where IsFloatColl<Element>: Any {
    func doSomething() {
        // type-checking works.
        self.first?.first?.advanced(by: .pi)
    }
}

func genericFunc_1D<T>(_ t: T) where IsFloatColl<T>: Any {
  t.first?.advanced(by: .pi)
}

func test() {
    let flt2D: [[Float]] = []
    let dbl2D: [[Double]]  = []
    let int2D: [[Int]] = []
    flt2D.doSomething() // Works.
    dbl2D.doSomething() // Works.
    genericFunc_1D(flt2D[0]) // Works.
    genericFunc_1D(dbl2D[0]) // Works.
    
    // int2D.doSomething()
    // Referencing instance method 'doSomething()' on 'Collection' requires that 'Int.Stride' (aka 'Int') conform to 'BinaryFloatingPoint'
    
    // genericFunc_1D(int2D[0])
    // Global function 'genericFunc_1D' requires that 'Int.Stride' (aka 'Int') conform to 'BinaryFloatingPoint'
}

It's surprisingly complete - constrained extensions and generics work, and even diagnostics see through the hack and provide good error messages. It can be a big help if you need to repeat long strings of constraints.

9 Likes

I'm not sure I understand what the expected behavior is. The left hand side of a where clause is a type parameter, rooted in a generic parameter of the type alias itself or an outer context. In this case the type alias does not have any generic parameters, and I'm assuming its not in a generic context, so the name RawSignificand is not in scope. If you want to refer to the associated type RawSignificand of a protocol, you need a base type, eg T.RawSignificand, and then add a generic parameter T to the type alias.

So while I'm sure it would be possible to give meaningful semantics to this example, I would not say it's an obvious feature, and certainly not a bug :)

The expected behavior is to make an alias for a type.

I want to write things like:

extension Randomizable {
  static func unitRandom() -> Self {
    return Self.random(in: 0..<1)
  }
}

func foo<T: Randomizable>() -> T {
  return T.unitRandom()
}

Instead of the more verbose:

extension BinaryFloatingPoint where RawSignificand: FixedWidthInteger {
  static func unitRandom() -> Self {
    return Self.random(in: 0..<1)
  }
}

func foo<T: BinaryFloatingPoint>() -> T where T.RawSignificand: FixedWidthInteger {
  return T.unitRandom()
}
2 Likes