Compiled Chronicles

A software development blog by Angelo Villegas

Swift: Phantom Types

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>'

Comments

  1. Ian Bellomy Avatar
    Ian Bellomy

    Oh! This would be super handy for clarifying radian / degree uses!

  2. Jayesh Kawli Avatar
    Jayesh Kawli

    For some reason this example it not compiling with Swift 3.0. Any help? It says use of undeclared type T while adding infix operator.

Leave a Reply

Your email address will not be published. Required fields are marked *