I found a weird and interesting use case for `PhantomData`
Isn't it wonderful when a niche tool you know about but would rarely ever touch just so happens to be the perfect solution for a completely unrelated problem? This just happened to me lol.
Normally you would only use PhantomData for type erasure and lifetime shenanigans. But today while coding a UI widget that has none of that stuff, surprisingly I found myself in a weird situation where PhantomData is exactly what I needed.
This is my existing code, minimised:
use std::fmt::Display;
pub struct DropDownMenu<T> {
pub selected: T,
}
impl<T: strum::IntoEnumIterator + Display> DropDownMenu<T> {
pub fn render(&self) {
for choice in T::iter() {
render_item(choice);
}
}
}
Now I need to change it so that DropDownMenu can also accept an arbitrary list of choices at runtime, say a Vec<String>. But it still needs to work with enums. So naturally I reached for traits:
pub trait ToChoices {
type Choice: Display;
fn get_choices(&self) -> impl Iterator<Item = Self::Choice>;
}
// this would actually be a generic impl, but let's keep it simple here
impl ToChoices for Vec<String> {
// again, I would actually use &str, but let's not involve lifetimes
type Choice = String;
fn get_choices(&self) -> impl Iterator<Item = Self::Choice> {
self.iter().cloned()
}
}
// workaround for "conflicting impl because upstream may add a new impl" problem
// specialization would make this unnecessary
pub trait ChoosableEnum: strum::IntoEnumIterator + Display {}
impl<T: ChoosableEnum> ToChoices for T {
type Choice = T;
fn get_choices(&self) -> impl Iterator<Item = Self::Choice> {
T::iter()
}
}
pub struct DropDownMenu<T: ToChoices> {
pub choice_provider: T,
pub selected: T::Choice,
}
impl<T: ToChoices> DropDownMenu<T> {
pub fn render(&self) {
for choice in self.choice_provider.get_choices() {
render_item(choice);
}
}
}
See the problem here? In making DropDownMenu contain a runtime value, its users are forced to always pass in an arbitrary value for choice_provider when constructing it. But in the case of a ChoosableEnum, such a runtime "provider" is not actually necessary. So how do you semantically encode the concept "for some types a runtime provider is necessary but for some others it's not"?
PhantomData to the rescue! Simple change and viola:
impl<T: ChoosableEnum> ToChoices for PhantomData<T> {
type Choice = T;
fn get_choices(&self) -> impl Iterator<Item = Self::Choice> {
T::iter()
}
}
Now constructing a DropDownMenu<MyEnum> is just DropDownMenu { choice_provider: PhantomData, selected: MyEnum::DefaultChoice }, which can be made nicer even further by wrapping it in a constructor associated function.
Honestly, taking a step back after going through all this, I must say, what a monstrosity 😅. But well, generic code does tend to grow into this when the requirement grows complicated so I guess it's not too bad after all.
Suggestions for improvements are very welcomed!
Turns out, I have in fact dug myself into a hole; see my comment. Alright, maybe this weird use of PhantomData is stupid after all. Let this be a cautionary tale then. Of what? I don't know. I should get some sleep.