Enum specialization?

Is it possible? I want to achieve an API like this

enum Unit {
    Kelvin, Celsius, Fahrenheit 
}

struct Temperature {
    // ...
}

let k = Temperature::<Unit::Kelvin>::new(23.11);
let c = k.to::<Unit::<Celsius>>();

I think this will be possible in the future with const generics, for now you can do this:

trait Unit {}
enum Kelvin {}
impl Unit for Kelvin {}
enum Celsius {}
impl Unit for Celsius {}
enum Farenheight {}
impl Unit for Farenheight {}

struct Temperature<U: Unit> { /* ... */ }
impl<U: Unit> Temperature<U> {
    fn to<V: Unit>(self) -> Temperature<V> {
        /* ... */
    }
}

let k = <Temperature<Kelvin>>::new(23.11);
let c = k.to::<Celsius>();

You will have to add methods to Unit as appropriate (probably just to_kelvin and from_kelvin).

Be careful - trying to create a value of the uninhabited enum type is an instant UB. For marker types, it's probably better to use unit structs, not uninhabited enums.

2 Likes

Why? I prefer using uninhabited enums to make it explicitly clear that it's not supposed to be used as a value. Otherwise it can confusing, people might accidentally accept a value of it as a parameter, or they might store a local with its type, both of which are meaningless.

1 Like

Isn't that still possible with uninhabited enums?

Actually yes, that's true. But at least when looking at the code or reading the docs an uninhabited enum makes it abundantly clear that it's not supposed to be used that way. And when the author attempts to use or write tests for this incorrect function they will run into problems and realize the mistake.

Zero-sized struct struct Foo; are made exactly for that kind of use case, compile time specialization. They are basically free because they'll be "compiled away". For example, that's how parts of chrono work, Utc and Local are both zero-sized structs with special traits implemented for them. The public API of chrono looks like Utc::now() -> DateTime<Utc>.

IMO, Temperature<Unit> looks very similar to DateTime<Offset>.

Using a unit type allows more versatility in how the marker is used. For example, you can use dynamic dispatch as well as static dispatch:

struct Temperature<U: ?Sized> {
    value: f32,
    unit: U,
}

fn takes_any_unit(_arg: &Temperature<dyn Unit>) {
    // this code doesn't have to be monomorphized and could be part of an object-safe trait
}

And you could bring back that enum for a different kind of runtime polymorphism:

enum Dynamic {
    K(Kelvin),
    C(Celsius),
    F(Fahrenheit),
}

impl Unit for Dynamic { /* whatever goes here, with a `match` probably */ }

fn takes_unknown_unit(_arg: Temperature<Dynamic>) {}

They also permit you to write functions that take values and infer the type, like let k = Temperature::new(Kelvin, 23.11) instead of <Temperature<Kelvin>>::new

Void types (empty enums) don't work with any of that, and don't really buy you anything extra for normal use cases.

I will refer to why you should not put bounds in structs for why I took : Unit out of Temperature, although mine seems to be the minority position there.

2 Likes

Ah, that's a good point. I think that if you know you won't be dynamically changing the format an enum {} would still be better, but I agree - for the general case a unit struct is more flexible.

I don't want to stray off topic too much - but do you know the story of Flatten? What happened was that the standard library placed minimally restrictive bounds on FlatMap - not realizing that it actually leaked implementation details of the type. It was only when Flatten was added and they attempted to implement FlatMap in terms of it that the mistake was realized, and now we're stuck with a messy FlattenCompat hack used inside both. It doesn't really apply here - I think that both having the bound or not would be absolutely fine - but I think you should edit your answer to explain how to avoid situations like that.

2 Likes

Thanks, I was aware of FlattenCompat but I didn't think of it while writing that answer. I'll give some thought to revising it. Nevertheless, I still feel it's a mistake to write unnecessary bounds in the majority of cases.

Kestrer was not trying to create a value of an uninhabited type. The method has the signature Temperature<U> -> Temperature<V>, not U -> V.

mod quantities {
    use std::marker::PhantomData as Ph;
    use std::fmt;

    pub trait Unit {
        const NAME: &'static str;
    }

    #[derive(Clone, Copy)]
    pub struct Temperature<U: Unit> {
        value: f64, unit: Ph<U>
    }
    impl<U: Unit> From<f64> for Temperature<U> {
        fn from(x: f64) -> Self {Self {value: x, unit: Ph}}
    }
    impl<U: Unit> fmt::Display for Temperature<U> {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{} {}", self.value, U::NAME)
        }
    }

    #[derive(Clone, Copy)] pub struct Kelvin;
    #[derive(Clone, Copy)] pub struct Celsius;
    impl Unit for Kelvin  {const NAME: &'static str = "K";}
    impl Unit for Celsius {const NAME: &'static str = "°C";}
    
    impl Temperature<Kelvin> {
        pub fn to_celsius(self) -> Temperature::<Celsius> {
            Temperature::<Celsius>::from(self.value - 273.15)
        }
    }
}

use quantities::{Temperature, Kelvin};

fn main() {
    let k = Temperature::<Kelvin>::from(23.11);
    let c = k.to_celsius();
    println!("{} = {}", k, c);
}
1 Like

I don't see the need for PhantomData if the Unit types are zero-sized anyway. In fact, it seems like the worst of both worlds: you still can't implement Unit for a dataful type and use it dynamically like I showed in my earlier post, but you also don't have any advantage of using void types.

Having NAME as an associated constant admittedly makes the dynamic approach less useful, but that doesn't really justify the PhantomData abuse IMO.

1 Like
mod hkt {
    pub trait AppliedTo<X: ?Sized> {type Value: ?Sized;}
    pub type App<F, X> = <F as AppliedTo<X>>::Value;
}

mod quantities {
    use std::fmt;
    use crate::hkt::AppliedTo;

    pub trait UnitValue {
        fn value() -> Self;
    }
    pub trait Unit {
        fn name(&self) -> &'static str;
    }
    pub trait Quantity<T> {
        fn value_unit(&self) -> (T, &'static str);
    }

    pub struct TemperatureType;
    impl<U: ?Sized> AppliedTo<U> for TemperatureType {
        type Value = Temperature<U>;
    }

    #[derive(Clone, Copy)]
    pub struct Temperature<U: ?Sized> {
        value: f64, unit: U
    }
    impl<U: Unit + ?Sized> Quantity<f64> for Temperature<U> {
        fn value_unit(&self) -> (f64, &'static str) {
            (self.value, self.unit.name())
        }
    }

    impl<U: UnitValue> From<f64> for Temperature<U> {
        fn from(x: f64) -> Self {Self {value: x, unit: U::value()}}
    }
    impl std::ops::Mul<Kelvin> for f64 {
        type Output = Temperature<Kelvin>;
        fn mul(self, rhs: Kelvin) -> Self::Output {
            Self::Output {value: self, unit: rhs}
        }
    }
    
    impl<U: Unit + ?Sized> fmt::Display for Temperature<U> {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            let (value, unit) = self.value_unit();
            if let Some(prec) = f.precision() {
                write!(f, "{:.*} {}", prec, value, unit)
            } else {
                write!(f, "{} {}", value, unit)
            }
        }
    }

    #[derive(Clone, Copy)] pub struct Kelvin;
    #[derive(Clone, Copy)] pub struct Celsius;
    impl Unit for Kelvin  {fn name(&self) -> &'static str {"K"}}
    impl Unit for Celsius {fn name(&self) -> &'static str {"°C"}}
    impl UnitValue for Kelvin {fn value() -> Self {Self}}
    impl UnitValue for Celsius {fn value() -> Self {Self}}

    impl Temperature<Kelvin> {
        pub fn to_celsius(self) -> Temperature::<Celsius> {
            Temperature::<Celsius>::from(self.value - 273.15)
        }
    }
}

use quantities::{Kelvin, Unit, Quantity, TemperatureType};
use hkt::{App, AppliedTo};

fn print_quantities<Q, T>(a: &[&App<Q, dyn Unit>])
where
    Q: AppliedTo<dyn Unit>, Q::Value: Quantity<T>,
    T: std::fmt::Display
{
    for t in a {
        let (value, unit) = t.value_unit();
        println!("{:10.2} {}", value, unit);
    }
}

fn main() {
    let k = 23.11*Kelvin;
    let c = k.to_celsius();
    println!("{:.2} = {:.2}", k, c);
    println!("Temperatures:");
    print_quantities::<TemperatureType, f64>(&[&k, &c]);
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.