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).
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)
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:
You need to do this for all the possible coercions that you need
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 unsafeBitCastshould 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:
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.
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:
Explicit but syntactically light, zero-runtime-cost coersion to/from the "base" 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.