How to tell compiler that [any P]'s associatedtype also conforms to another protocol

Hi all, I'm trying to figure out the best way to constrain an associated type of an [any P] in order to enable additional functionality and I'm not sure of the best way to do this. Here is a very simplified example using stdlib types:

protocol P {
	associatedtype T: Numeric
	associatedtype S: StringProtocol

	var number: T { get }
	var string: S { get }
}

extension P {
	func example1() -> T.Magnitude {
		number.magnitude
	}
}

extension P where T: FloatingPoint {
	func example2() -> T.Exponent {
		number.exponent
	}
}

struct S1: P {
	var number: Double
	let string = "S1"
}

struct S2: P {
	var number: Int
	let string = "S2"
}

struct S3: P {
	var number: Float
	let string = "S3"
}

let arrayOfP: [any P] = [	// if I can constrain P.T: FloatingPoint, example2() should become available, but how?
	S1(number: 5.5),
	S3(number: 1.3),
	S1(number: 2.0)
]

let s1 = S1(number: 4.2).example2()
let s3 = S3(number: 2.3).example2()

arrayOfP.forEach {
	$0.example1()
	$0.example2()	// how to get this to work?
}

The most straightforward way to do this (assuming that you want exactly what you say, in which case a primary associated type would be insufficient) would be to declare a refining protocol Q: P where T: FloatingPoint, conform the relevant types to Q, and then make your array of type [any Q].

3 Likes

Thanks! I thought about that, but was hoping for something that didn't require types (of which I may not control) to remember to conform to Q if number is a FloatingPoint. I'll go this route if nothing better comes along... :slight_smile:

If you control Q, it’s legitimate to conform any type you encounter retroactively to Q (as long as the requirements are met of course).

2 Likes

But only if I have the original type to allow a conformance. (thinking through edge cases right now) If I have a library where the original type might pass unknown through an api as an any. For example say the api is this:

func callFuncsOnP(_ p: any P)

I can unbox that and call example1(), but there still isn't a way to call example2() that I can see. I can make sure that number is a FloatingPoint by casting, but compiler won't propagate that knowledge to the type as far as I can tell.

Your situation sounds nightmarish, but I think it will be improved once the language evolves to the point where type erasure isn't necessary past one opaquing level.

let anySequence: AnySequence<some FloatingPoint> = .init([0])

// …should be replaceable by the following, but it won't compile yet.
// 'some' types are only permitted in properties, subscripts, and functions
let someSequence: some Sequence<some FloatingPoint> = [0]

I think that the overall solution would be the ability to add where clauses to generics (type or protocol) at any point of use. So:

var a: (any P where P.T: FloatingPoint) // compiler can prevent other any P assignments and has more type info

if let a = a as? (some P where P.T: FloatingPoint) {
	// compiler has more type info
}

struct S<T: Numeric> {
	let number: T
}

let s = S(number: 1.0)

if s is (S where T: FloatingPoint) {
	// compiler has more type info and we can now use number as a FloatingPoint as well
}

I'm not sure how difficult this change would be since I haven't looked at what type info the ABI is already encoding. If it already has this information available, then it is just a matter of adding syntax. Otherwise, Swift 6! :slight_smile:

2 Likes

@xwu do you have any idea of how difficult this change would be?

Extremely difficult, a full-fledged feat of engineering.

Oof... that's too bad. :frowning:

1 Like