Playing around Swift for some time and you might have encountered a bug or errors on your code by summing up or multiplying variables with different types. Well, not really just Swift but a lot of programming languages have the possibility to calculate variables of different types resulting to possible bugs and/or errors.
Phantom Types try to eliminate these possible errors by invalidating states that are irrepresentable.
What is a Phantom Type?
A phantom type is a parametrised data which contains extra hidden generic parameters that holds no storage. It can be used to mark values at compile time. Because they are specialisations of more general type, the advantage is writing a function that works by accepting the general type.
But Swift is already a strongly typed language?
Sometimes, we want to have additional safety when dealing with important types in our code. The more powerful the type system is, the more you can express on it.
What are they good for?
Phantom types are useful when you have different kinds of data that have the same representation but should not be mixed. There are a lot of things that have this description, currency for example: different data, same representation.
struct Euro {}
struct Peso {}
struct Dollar {}
We add three empty types, which we will not do anything at value level, but records information on the type-level.
After that, we created a new structure named CurrencyT
with a generic parameter. The CurrencyT
structure has one stored property, amount
, which is of type Double
:
struct CurrencyT<T> {
private let amount: Double
init(amount: Double) {
self.amount = amount
}
}
The structure defines a single initialiser with one parameter that accepts objects of type Double. We can then create an infix operator that accepts two parameters that is of type CurrencyT
:
func +<T>(left: CurrencyT<T>, right: CurrencyT<T>) -> CurrencyT<T> {
return CurrencyT(amount: left.amount + right.amount)
}
This function add two objects of type CurrencyT
and return the sum.
A Ghost in the Shell
We will use the previous codes to initialise a constant called dollarAmount
:
let dollarAmount = CurrencyT<Dollar>(amount: 9.99)
and add it up twice:
let newAmount = dollarAmount + dollarAmount
Note: Don’t forget to print
:
print(newAmount)
By running the code, it will print out:
CurrencyT<Dollar>(amount: 19.98)
Extending the situation
We can also add an extension
that will extend the type Double
to use the Peso sign (₱) to explicitly say that the variable is of type CurrencyT<Peso>
.
extension Double {
var ₱: CurrencyT<Peso> {
return CurrencyT<Peso>(amount: self)
}
}
Note: Peso is the currency of the Philippines and we will use it in this example.
By adding an infix
addition operator, we were able to sum up objects of type CurrencyT
. Now we will add another infix
operator that will multiply a CurrencyT
to a value of type Double
.
func *<T>(left: CurrencyT<T>, right: Double) -> CurrencyT<T> {
return CurrencyT(amount: left.amount * right)
}
Let’s just say a full year of Netflix subscription, which has been recently released to 130 countries:
let pesoAmount = 470.0.₱
print(pesoAmount * 12)
After running the code above, it will give the result:
CurrencyT<Peso>(amount: 5640.0)
Real World Problems
When dealing with real world applications, things like this should be rare than a blue moon, if not at all. Adding phantom types may eradicate the problem of solving a solution that uses the same expression with different data type.
One real world example is using distance, you could have distance tagged with it’s length unit.
Let’s put on some good show, this time we will declare an empty protocol
:
protocol Distance {}
After declaring a protocol, we will declare an empty enum
that uses our protocol Distance
to define our length unit, Kilometres
and Miles
:
enum Kilometres : Distance {}
enum Miles : Distance {}
One interesting fact about the two values is that you cannot create a value of the type Kilometres, or Miles because it is an enumeration with zero possible values, well, at least not yet.
We will then declare two structures:
struct ConvertUnit<D: Distance> {
private let distance: NSDecimalNumber
init(distance: NSDecimalNumber) {
self.distance = distance
}
}
struct LengthUnit<D: Distance> {
private let distance: Double
init(distance: Double) {
self.distance = distance
}
}
ConvertUnit
is a struct
that we will use to convert Miles
to Kilometres
and LengthUnit
is a struct
that we will use for arithmetic operations. ConvertUnit
is defined with one stored property named distance
with type NSDecimalNumber
while LengthUnit
is defined with one stored property named distance
with type Double
.
func +<T>(left: LengthUnit<T>, right: LengthUnit<T>) -> LengthUnit<T> {
return LengthUnit(distance: left.distance + right.distance)
}
Then, we created an infix operator
just like the previous one, but this time instead of accepting CurrencyT
, we will accept LengthUnit
as type for the two parameters.
func convertDistance(km:ConvertUnit<Kilometres>) -> ConvertUnit<Miles> {
let converted: NSDecimalNumber = NSDecimalNumber(mantissa: 621371, exponent: -6, isNegative: false)
return ConvertUnit(distance:km.distance.decimalNumberByMultiplyingBy(converted))
}
1 km is equal to 0.621371 mi
The code above will convert the distance of type ConvertUnit<Kilometres>
to type ConvertUnit<Miles>
. To do so, we used NSDecimalNumber
using a mantissa
of 621371 with exponent -6 and defined it as not negative.
And of course, don’t forget our extensions
extension Double {
var km: LengthUnit<Kilometres> {
return LengthUnit<Kilometres>(distance: self)
}
var mi: LengthUnit<Miles> {
return LengthUnit<Miles>(distance: self)
}
}
By giving our length unit their own type, we can easily distinguish which is which. Let’s declare a constant of type Kilometres
:
let distanceKM = 220.0.km // LengthUnit<Kilometres>
We will declare another constant that will hold the converted value from this constant:
let distanceMI = ConvertUnit<Kilometres>(distance: NSDecimalNumber(double: distanceKM.distance))
And printing the code above will result to:
ConvertUnit<Miles>(distance: 136.70162)
Avoiding a disaster
Using phantom types are not necessary, you don’t even need to learn it if you don’t want to. One thing I’ll tell you though, they could have used Phantom Types and avoided the Mars Climate Orbiter disaster.
By using phantom types, this code:
print(distanceKM + distanceMI)
will result to an error similar to:
Binary operator '+' cannot be applied to operands of type 'LengthUnit<Kilometres>' and 'LengthUnit<Miles>'
Leave a Reply