Using Phantom Types in Swift
Phantom types are special types in Swift. They are useful when you want to pass the variable with same structure but with different types. I would like to thank to This blog post for inspiring me to write this post. Also examples mentioned in the post are inspired by the above mentioned blog post.
Phantom types are useful when you want to pass or perform actions with predefined types or variables which are of same type but conform to different protocols. For example, say you have distances in two units, Km and m and have a method which adds up these units. Now you either want to add Kilometers to Kilometers or miles to miles, but not miles to kilometers or vice versa. Phantom types can help you prevent this common confusion by raising compiler error if such attempt is made.
To give another example, say you want to convert kilometers to miles and function should only input kilometer unit and should throw compiler error out when wrong type is passed. Phantom types can help in these situations too.
How? Let's see that as follows,
Say you have two units of distances. Kilometer and Mile. Let's represent them in terms of LengthUnit
// Distance is the protocol which is conformed by both Kilometers and Miles
protocol Distance {
var conversionFactor: Double { get }
}
enum Kilometers: Distance {
var conversionFactor: Double {
return 0.625
}
}
enum Miles: Distance {
var conversionFactor: Double {
return 1.6
}
}
struct LengthUnit<T: Distance> {
let distance: Double
init(distance: Double) {
self.distance = distance
}
}
We also want an ability to add up two LengthUnit
variables. so let's start with operator overloading to add up two LengthUnit
objects having same type
func +(left: LengthUnit<T>, right: LengthUnit<T>) -> LengthUnit<T> {
return LengthUnit(distance: left.distance + right.distance)
}
The above code specifies you can add two LengthUnit
objects as long as they are uniform. Either LengthUnit<Kilometers>
or LengthUnit<Miles>
. So following line will work
let totalMiles = LengthUnit<Miles>(distance: 10.0) + LengthUnit<Miles>(distance: 20.0)
let totalKilometers = LengthUnit<Kilometers>(distance: 10.0) + LengthUnit<Kilometers>(distance: 20.0)
But not following,
let totalDistance = LengthUnit<Miles>(distance: 10.0) + LengthUnit<Kilometers>(distance: 20.0)
It will result in compiler error as follows,
So phantom types actually prevent you from causing intractable bugs resulting from passing wrong types to function
Let's look at one more example. Let's say you want to convert one unit to another. In order to do that we need to multiply variable of type LengthUnit<T>
with constant. (Based on what type of conversion that is!). In order to do that, let's overload *
operator which takes LengthUnit<T>
and Double
as an arguments and return the variable of type LengthUnit<T>
back. (So if you pass LengthUnit<Miles>
, you will get LengthUnit<Kilometers>
back etc.)
func *(left: LengthUnit<T>, right: Double) -> LengthUnit<T> {
return LengthUnit<T>(distance: left.distance * right)
}
Now, we will define two methods. One to convert Kilometers to Miles and other to convert Miles to Kilometers. We will explicitly specify in method body which type is associated with LengthType
// Only takes LengthUnit parameter with Kilometers type
func convertKilometers(distance: LengthUnit<Kilometers>) -> LengthUnit<Miles> {
return LengthUnit<Miles>(distance: distance.distance * 0.625)
}
// Only takes LengthUnit parameter with Miles type
func convertMiles(distance: LengthUnit<Miles>) -> LengthUnit<Kilometers> {
return LengthUnit<Kilometers>(distance: distance.distance * 1.6)
}
Now with these methods you can easily convert LengthUnit with one type into LengthUnit of another type as follows,
let kmToMiles = convertKilometers(distance: LengthUnit<Kilometers>(distance: 4.5)) // Returns type LengthUnit<Miles>
let milesTokm = convertMiles(distance: LengthUnit<Miles>(distance: 4.5)) // Returns type LengthUnit<Kilometers>
Now, it actually prevents you from passing LengthUnit of wrong type by mistake since we explicitly specify the type we are expecting as a method input. Suppose you make a mistake and pass something like this,
let kmToMiles = convertKilometers(distance: LengthUnit<Miles>(distance: 4.5))
It will show following error since method expects LengthUnit<Kilometers>
and throws out following compiler error.
I am pretty sure there are going to be lot of real life cases where phantom types could be life saver. Use if where you are dealing with multiple type information and passing correct type is important to avoid any potential bugs. If you think of any interesting cases where phantom types could be useful, don't hesitate to get back to me