Coerce phantom types

Using phantom types is a nice way to add some extra type safety to the code. (for example see GitHub - pointfreeco/swift-tagged: 🏷 A wrapper type for safer, expressive code.). I'm using this in multiple places, but I often run into cases where I would like to coerce one type to another by just changing the shadow type. I know Haskell has coerce, which can do this without runtime overhead. Is there a way to do this in Swift?

This doesn't work:

struct Blah<T> {
var name: String
}

let b: Blah<String> = Blah<Int>(name: "something") as! Blah<String>

It will produce this:

Cast from 'Blah' to unrelated type 'Blah' always fails
error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).

Can you link to something on shadow types? I can't find anything on them.

Also what ought your code sample to do?

Ugh, I wrote shadow types instead of phantom types, which is what I actually meant.

Basically, a phantom type is a generic type parameter that is not used in the type. Here Blah is not a type, it's a type constructor that produces a type for any type that is passed to it: Blah, Blah, etc. My code example was bad, it's just the only thing I could think to make this work. .This is more what I meant:

let b: Blah<String> = coerce(Blah<Int>(name: "something"))

In this way Blah would still be a different type from Blah, but I could convert one to the other without runtime overhead; instead of what I'm doing now:

let a = Blah<Int>(name: "something")
let b = Blah(name: a.name)

https://www.natashatherobot.com/swift-money-phantom-types/

How I would imagine doing this is either:

extension Blah {
  @inlinable
  init<U>(_ other: Blah<U> { /* assign properties */}
}

Or the following if you want to restrict the source / destination types:

extension Blah where /* insert constraints here */ {
  @inlinable
  init(_ other: Blah<Int> { /* assign properties */} // Int as example
}

Basically, treat it as any other initializer and trust the compiler to remove the copies.

1 Like

Yes, that's what I'm doing now. I'm not sure what is the runtime overhead, but in any case having to copy all the properties from one object to the new one is not nice:

  1. You need to do this for all the possible coercions that you need
  2. You need to remember to change all these methods every time you add a property

I'm guessing this should be a proposal in the evolution forum (which would be nicely complemented by a newtype implementation), but I wanted to know if there is currently any other solution.

I think the init solution is definitely far, far better, but unsafeBitCast should do what you want here. Be warned that if you do use that approach you’re subject to compiler/runtime implementation details and might run into odd bugs and crashes (e.g. in generic contexts where the concrete type isn’t known).

I think this is a bad solution to a real problem. There are issues I have with this approach:

  1. All types need a name. However, it's often hard to come up with an appropriate name for something so generic. In some use cases, like type-specific IDs, there are natural names, like ID<SomeType>. In other cases, the base name is less obvious, but nonetheless necessary for forming a phantom type.
  2. These types differ by virtue of their generic parameter, but that generic parameter isn't actually used anywhere. It's just a hack to distinguish types.

I think a much better approach would be to introduce something like Haskell's newtype keyword. It's like Swift's typealias, but with the key distinction that the type system treats the newtype and the original type as distinct. Here's how it might behave (using the dummy spelling of newtype, but we can use something else):

newtype FirstName = String
newtype LastName = String

struct Person {
    let firstName: FirstName
    let lastName: LastName
}

protocol UI {
    firstName: FirstName { get }
    lastName: LastName { get }
}

let person = Person(firstName: ui.firstName, lastName: ui.firstName) // ❌
// error: cannot convert value of type 'FirstName' to specified type 'LastName' 

Some other desirable behaviors:

  1. Explicit but syntactically light, zero-runtime-cost coersion to/from the "base" type:

    func printString(_: String) { /* ... */ }
    
    printString(firstName as String)
    printString(firstName) // ❌
    
  2. Explicit but syntactically light, zero-runtime-cost coersion to/from other types with the same "base" type:

    func printFirstName(_: FirstName) { /* ... */ }
    
    printFirstName(lastName as FirstName)
    printFirstName(lastName) // ❌
    
  3. Inheritance of all conformed protocols of the base types, including the ExpressibleByXLiteral ones.

    let firstName: FirstName = "Joe" // Uses inherited ExpressibleByStringLiteral conformance
    let names: [FirstName: LastName] = [ // Uses inherited Hashable and Equatable conformances
       "Joe": "Smith",
       "Bob": "The Builder",
    ]
    
  4. Direct access to all of the "base" types functions, properties and operators:

    let uppercasedFirstName: FirstName = firstName.uppercased() // "JOE"
    
2 Likes

Just out of curiosity, would this give the original String or a 'substituted' FirstName as the result type?

@torquato I think it would make sense to return a FirstName.

The reason I suggested this direct access to base type (e.g. String) members, modified to return the newtype type (e.g. FirstName) is to avoid this pattern:

let capitalizedFirstName = (firstName as String).uppercased() as String

Yes, I agree that newtype would be a better solution, but that's not what we have today. My question is, given what is possible right now, is there a way to coerce one type to another type with the same representation. I knew that it probably wasn't possible without resorting to something unsafe like unsafeBitCast or something like that, but I just wanted to ensure that I wasn't missing anything.

unsafeBitCast is well-defined for structs and enums as long as the source and destination have the exact same type layout. (For class references, there are type-based pointer aliasing rules that could potentially introduce undefined behavior if you modify a class instance through a reference with the wrong type.) For a struct or enum with phantom type parameters, the layout will never change dependent on those parameters, so this should be OK.

4 Likes

Thanks, I ended up using unsafeBitCast, wrapped so that it can be used safely:

extension Tagged {
  public func coerced<Tag2>(to type: Tag2.Type) -> Tagged<Tag2, RawValue> {
    return unsafeBitCast(self, to: Tagged<Tag2, RawValue>.self)
  }
}

I made a pull request to swift-tagged, and it will be available when they release a new version.

1 Like