Phantom Types in Rust 👻
Serhii Potapov October 11, 2021 #rust #patterns #phantom typeProblem introduction
Let's say we have simple wrapper around f64
that encodes distance in meters:
In our application we want to be able to add and subtract distances, so we would implement Add
and Sub
traits for convenience:
use ;
A quick manual test proves it works as expected:
Output:
&four = Meter { value: 4.0, }
&two = Meter { value: 2.0, }
But later we may decide that we need to introduce similar types: Kilogram
to measure weight, Liter
to measure volume, and so on.
For each of the types, we have to implement the same traits again and again. Of course, we can use macros, but they must be the
last resort .
So,
is there a better way?
Welcome phantom types 👻
Phantom types are such types that are never used in runtime but help to enforce some constraints at compile time.
We could introduce a generic type Unit<T>
and implement Add
and Sub
for it.
Empty structs MeterType
and KilogramType
could be used to mark a particular unit.
And we could define type aliases Meter
and Kilogram
for convenience:
;
type Meter = ;
;
type Kilogram = ;
MeterType
and LiterType
are zero-sized types and have no runtime impact.
Sounds like a great idea, but the compiler is less excited than we are:
error[E0392]: parameter `T` is never used
--> src/main.rs:38:13
|
38 | struct Unit<T> {
| ^ unused parameter
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`
= help: if you intended `T` to be a const parameter, use `const T: usize` instead
The compiler does not to like the unused generic parameter. I appreciate this.
Let's add unit_type: T
field to make the structure use that T
type:
This solution seem to compile. Let's implement new()
constructor for Unit<T>
:
But now we have to provide a concrete value to initialize unit_type
.
Let's be stubborn and implement Default
trait for MeterType
and KilogramType
:
Now let's adjust new()
to use the default()
to set value for unit_type
:
Hooray! This compiles and we can proceed further implementing Add
and Sub
traits for newly born Unit<T>
type:
Now let us test it. We still can add meters as we could before:
Output:
&two_meters = Unit { value: 2.0, unit_type: MeterType }
But the most important, the compiler does not allow us to mix meters and kilograms now:
Error message:
--> src/main.rs:93:38
|
93 | let god_knows_what = one_meter + two_kilograms;
| ^^^^^^^^^^^^^ expected struct `MeterType`, found struct `KilogramType`
|
= note: expected struct `Unit<MeterType>`
found struct `Unit<KilogramType>`
Looks good. Are we done? Not yet!
Hello PhantomData
The problem above could be solved slightly easier with help of PhantomData from the standard library.
Actually, the compiler already gave us a hint, when we introduced Unit<T>
suggesting to use PhantomData
:
help: consider removing `T`, referring to it in a field, or using a
marker such as `PhantomData`
The documentation defines PhantomData as:
Zero-sized type used to mark things that “act like” they own a T.
And what is also relevant for us:
Though they both have scary names, PhantomData and ‘phantom types’ are related, but not identical. A phantom type parameter is simply a type parameter which is never used. In Rust, this often causes the compiler to complain, and the solution is to add a “dummy” use by way of PhantomData.
Indeed, all that we did before was the "dummy" use of the phantom type T
.
Let's use PhantomData
for this purpose now and adjust Unit<T>
and other code accordingly:
use PhantomData;
You can see the full code example in playground .
PhantomData gives us at least 2 benefits:
- We can use
PhantomData
to initializeunit_type
field. This means thatMeterType
andKilogramType
do not have to implementDefault
anymore. - With
PhantomData<T>
we communicate explicitly the purpose of the type parameter to other developers.
That is it for now. This was a little practical introduction to phantom types with Rust. Thank you for reading.
Dicussion on reddit.