Landing on Mars With PhantomData
I was browsing through the Rust standard library and came across PhantomData. I didn’t know what it was used for, so I started reading more about it.
Rust won’t allow you to declare a type parameter without using it. The solution is PhantomData. The PhantomData type can be used in situations when you really don’t need to store the value of something but rather, just the type. There are some examples in Rust’s documentation, but I thought I’d try one out for myself.
The Final Frontier 👩🚀
Let’s say you want to land on Mars and you have three teams, the Sensor, Thrust and System team. The Sensor and Thrust team’s responsibilities are pretty obvious. The System team is responsible for using the Sensor and Thrust team’s code to program the landing sequence.
The First Iteration 🚀
The combined effort of all the teams produced the following code.
Ok, so I don’t know any rocket science as you can tell. Despite having zero rocket knowledge I could still tell you that this is going fail — hard 💥.
Numbers without context 🇺🇸 🇸🇪 🇬🇧
It turns out the Sensor and Thrust teams are located in different countries, and it so happens to be countries with different measurement systems. One team uses meters and the other feet, that’s not good!
Each team internally agreed on a single measurement system, but never amongst teams. The Sensor team never realized the problem because they never directly interacted with the Thrust team’s code, and vice versa. The System team uses both the Sensor and Thrust team’s code, but has limited knowledge about the inner workings of the other team’s code. They happily use the code as any other library.
The teams could try and decide on what measurement unit to use when, but this is no easy thing. The teams are big and lots of people contribute to the code bases. It’s human error waiting to happen.
The Second Iteration ♻️
Let’s explore a different solution centered around PhantomData.
So what’s going on here?
Found Some Context 🕵️♀️
The second iteration is centered around the Float64<T> type. An issue with the previous code was that the measurements were sent around as an ordinary f64. There’s really no context attached to the f64, it could be a number representation of pretty much anything. Instead, we create our own type with a type parameter T.
With the type parameter T we can say that Float64<T> actual is a Float64<Meter>. Here’s where PhantomData comes in. We don’t actually want to declare a field t: T on our Float64 type, so we instead use a PhantomData field. What’s nice is that the PhantomData type is zero-sized, so we gain semantics without taking up extra memory.
Help The Human 🤖
Notice that each team now receives and returns specific versions of the Float64<T>, some only work with meters and others feet. The real kicker here is that the example above will not compile. This is great :). The System team can now be more confident that they are passing around the right type of values and finally land on Mars.
This is really interesting. Something that could’ve become a runtime error is now pushed back in the development stage and is instead highlighted as a compile-time error. This could in the right situation be a bug-squashing, code quality, time saver.
Sugar The Type 🍯
Since we’ve wrapped a regular f64 in our Float64 type it can be a bit cumbersome to work with, for example doing mathematical operations. So in the example I’ve added an implementation for the std::ops::Mul trait for the Float64<T>, we can now use the *(multiplication operator) with our wrapper type. If we kept on going and implemented more of the std::ops traits we could treat this almost as if it were a regular f64.
Some Thoughts 🤔
While I was exploring solutions for this example I realized that there are many ways to solve the problem, and using PhantomData might not be the most obvious or conventional way. Regardless, I liked this thought experiment and hopefully some of you do to :)