Why marker types and not concrete values?
sunshowers opened this issue · comments
Our current model is what I'd refer to as the "marker model":
struct TypedUuid<T> {
uuid: Uuid,
marker: PhantomData<T>,
}
One kind of type that isn't representable in the marker model is a UUID that's "either this or that".
For example, you could imagine something like this, which I'll call the "concrete model":
struct TypedUuid<T> {
uuid: Uuid,
type_info: T,
}
enum FooOrBar {
Foo,
Bar,
}
impl TypedUuidKind for FooOrBar {
fn tag(&self) -> TypedUuidTag {
// match on self.type_info
}
}
While the concrete model is more flexible, it also carries some rather large downsides. The main one is that it complicates our APIs. For example, for an ergonomic API we'd need
impl<T: TypedUuidKind> TypedUuid<T> {
pub fn new_v4() -> Self where T: Default {
Self { uuid: Uuid::new_v4(), type_info: T::default() }
}
pub fn new_v4_with_info(type_info: T) -> Self {
Self { uuid: Uuid::new_v4(), type_info }
}
}
Similarly, the concrete model would also complicate the FromStr
impl, for which we have two options:
- Restrict it to
T: Default
. - Require that the input string specify a tag, which is a pretty large behavior change that can break lots of things if not done with care.
The GenericUuid
trait becomes more complex as well -- part of the benefit of the marker model is easily being able to write code that's generic over typed and untyped UUIDs. It's not clear how well that would survive in the concrete model.
Furthermore, the same result can be achieved in other ways most of the time -- users can just as well write their own enums "outside" the type param:
enum FooOrBarUuid {
Foo(TypedUuid<Foo>),
Bar(TypedUuid<Bar>),
}
And that would in many ways provide more clarity than putting the param inside the UUID. This approach would also let more data be associated with each variant, which is often required in practice.
Based on all this I'd like to keep the current marker model for now, and see how things evolve in the future.