Understanding the "cast from 'any P' to unrelated type 'S' always fails" warning

i wrote some code like the following:

public
struct S
{
}

public
func f(values:[any Identifiable<Int>])
{
    for case let value as S in values
    {
    }
}

my intention was to retroactively conform S to Identifiable<Int> in a downstream module. the goal was to have f be able to discover an instance of S only if the app includes a conformance of S to Identifiable<Int>.

but instead, i get this warning:

warning: cast from 'any Identifiable<Int>' to unrelated type 'S' always fails
    for case let value as S in values

is this something that is supported by the type system?

2 Likes

I believe the warning is a bit of lie—if you do provide that retroactive conformance then the cast will, in fact, succeed. Perhaps we ought to allow silencing this warning somehow when that's the author's intention, maybe by module-qualifying the types involved, as one can do to silence the retroactive conformance warning?

2 Likes

Having some explicit indicator that you really mean to do this would also be a good way to prevent the optimiser from eliminating the actual runtime check. Otherwise, I wouldn't trust future compiler versions to not optimise this out, irrespective of what the current version happens to do.

1 Like

This is an interesting problem. What should happen if that function gets called when no one supplied the conformance? Right now it would silently "fail" (skip over the Ss). Should it instead terminate the program?

I suppose the API of the module would ideally require someone to provide a conformance of S to Identifiable, but there's no way to do that in Swift. The closest we can get is adding a global hook that starts off failing at runtime (as in fatalError) with an instruction to replace it.

Since Identifiable has an associated type, and this hook implies the implementation (including the type of the ID that gets returned) is now set at runtime, you lose type safety and just fall back to type erased IDs (luckily the specially treated AnyHashable eraser makes this easier).

public var getSID: (_ s: S) -> any Hashable = {
  fatalError("You must supply a definition for the ID of an `S`")
}

public struct S: Identifiable {
  var id: AnyHashable { getSID(self) }

  ...
}

I have wanted the ability to leave unimplemented hooks like this that some other module must define many times. This is possible in C/C++ by supplying a header declaration for a function in a library (so the library implementation can include and call it) but not a definition. If the library is statically linked, the library compiles fine but once you include the library in an executable (either a program or a dylib), if you don't define the function you are hit with an undefined symbol error at link time. This is perfect, as long as you understand the error. It catches the mistake of not defining the function at build time.

If the library is dynamically linked, you need to turn on the compiler option to allow undefined symbols, and the error of not defining the function gets pushed to runtime as soon as the library gets loaded. This is less ideal, but if you truly need dynamic linkage (you usually don't, it's needed for e.g. a plugin system) this is the only way that makes sense. At least the error occurs as soon as the library is loaded and not whenever the function is first called.

Allowing Swift to do something like this would require this ability to wire up calls at link time, which enables link time polymorphism. I pitched what I believe is a more "Swifty" (instead of C-like) way to accomplish this a while ago here.

Another approach is to do this with generics. You can maintain strongly typed IDs that way:

protocol SIDProvider<ID> {
  associated type ID: Hashable

  static func id(for s: S<Self>) -> ID
}

struct S<IDProvider: SIDProvider>: Identifiable {
  var id: IDProvider.ID { IDProvider.id(for: self) }

  ...
}

public
func f<IDProvider: SIDProvider>(values:[any Identifiable<IDProvider.ID>])
{
    for case let value as S<IDProvider> in values
    {
    }
}

Now all the code up until the module that defines the ID needs to become generic, and by propagating the same type parameter this expresses that all the code works with a single ID Provider. Once the module defining the ID is present, you just assign that type parameter to the SIDProvider implementation defined in that module and everything above it is no longer generic. This expresses that this is where the ID definition gets locked down.

This is how I would solve this problem. It's more "invasive", lots of stuff (everything between where S enters the picture and the module defining the ID comes in) has to be modified but it's strongly typed and fully compile time enforced.

Another example where generics proliferate, which I found scary at first but eventually got used to it and now favor it and exercise them frequently and pervasively.

With the help of additional protocol there can be a compiler guarantee:

// module A
struct MyType: MyTypeProto {
}

protocol MyTypeProto {
}

func f<T>(
    values: [some Identifiable<Int>],
    _ t: T.Type = T.self
) where T: MyTypeProto & Identifiable {
    for case let value as T in values {
    }
}

// module B
import A

extension MyType: @retroactive Identifiable {
    public var id: Int { 1 }
}

f(values: [MyType()], MyType.self)  // a bit ugly on a call site, as type cannot be inferred otherwise
1 Like

This approach made me think, "could we put f in a constrained extension?", like this:

extension S where Self: Identifiable<Int> {
  static func f(values: [some Identifiable<Int>]) {
    for case let value as Self in values {
    }
  }
}

But this doesn't work, you get "where clause on non-generic declaration" error (same if you move the constraint to the function).

However, that seems like it would be a reasonable way to express in Swift what is desired here. Retroactive conformances are what would make these kind of "non-generic constraints" meaningful.

Along those lines I think in your approach you can eliminate the surrogate type parameter by moving f into an extension on the protocol:

extension MyTypeProto where Self: Identifiable<Int> {
  static func f<T>(
    values: [some Identifiable<Int>]
  ) {
    for case let value as Self in values {
    }
  }
}

You still have to spell out MyType to call it, but maybe it's a little nicer as MyType.f(values: ...) instead of as a function parameter.

I think a problem with this approach might be that you can't actually call f until you're in code where the ID definition is visible. Before that, MyType doesn't satisfy the constraint. And if you only need to call f after the retroactive conformance is introduced you could just move f itself up to that level.

You'd probably have to make code that calls f before the ID is defined generic w.r.t. MyTypeProto, i.e:

func somethingThatCallsF<T: MyTypeProto & Identifiable>() {
  ...

  f(values: ..., T.self)

  ...
}

So on this level, between MyType getting defined and the conformance getting defined, things will either need to become generic or lose type safety either way.

1 Like