Attempting to metaprogram a RawRepresentable deserializer inline does not work

Take a look at the below code:

enum EnumX {
  case foo
  case bar
}

protocol ProtocolX: RawRepresentable {
  init(rawValue: RawValue)
}

struct Foo: ProtocolX {
  typealias RawValue = Int
  var rawValue: RawValue
  init(rawValue: RawValue) {
    self.rawValue = rawValue
  }

}
struct Bar: ProtocolX {
  typealias RawValue = String
  var rawValue: RawValue
  init(rawValue: RawValue) {
    self.rawValue = rawValue
  }
}

struct SubscriptX {
  static subscript(_ enumX: EnumX) -> any ProtocolX.Type {
    switch enumX {
    case .foo: Foo.self
    case .bar: Bar.self
    }
  }
}

func deserializerX(type: (some ProtocolX).Type, pointer: UnsafeRawPointer) -> some ProtocolX {
  let rawValue = pointer.load(as: type.RawValue.self)
  return type.init(rawValue: rawValue)
}
let pointer = UnsafeRawPointer(bitPattern: 0x7f7f_7f7f_7f7f_7f7f)!

let type = SubscriptX[.foo]
// typed as `any ProtocolX`
let instance1 = deserializerX(type: type, pointer: pointer)
// typed as `some ProtocolX`
let instance2 = deserializerX(type: Foo.self, pointer: pointer)

// inline attempt
let type_ = SubscriptX[.foo]
let rawValue = pointer.load(as: type_.RawValue.self) // Type of expression is ambiguous without a type annotation
let instance3 = type_.init(rawValue: rawValue) // Member 'init' cannot be used on value of type 'any ProtocolX.Type'; consider using a generic constraint instead

The seralizerX function works just fine, albeit returning any ProtocolX instead of some ProtocolX when SubscriptX[.foo] is passed in, but I think that might just be by design. However, attempting to inline it (as seen on the bottom) causes multiple compiler errors.

Attempting to cast type_ as some instead of any like so:

let type_: (some ProtocolX).Type = SubscriptX[.foo]

gives the compiler error:

Cannot convert value of type 'any ProtocolX.Type' to specified type '(τ_0_0).Type'

Is inlining the behavior of deserializerX possible, or can this sort of interplay between some and any only be done with a helper function?

You are exactly right, and this is by design. You can pass a value of any P to a function that takes an argument of type some P—which is another way of spelling a generic parameter T where T: P. Inside the function, T is bound to the underlying type of the existential value, but it does not escape out of the function; rather, it is always "erased" to an existential.

You can read more about this in the proposal for this feature.

1 Like

Very interesting, thank you!

I think the one thing that still gets me is that, ultimately, this creates a situation in which these two implementations are not the same:

// Implementation 1

// Block A
// Block B

// Implementation 2

// Block A
foo(...) // contains Block B

It's something I would normally expect out of a programming language, but I guess it's never explicitly guaranteed. It seems, in this case, the syntax itself has side effects.