Implementing Zero trait for an enum type

I have a enum AnyNumber where I would like to implement the trait Zero. However, I'm getting a warning that the function zero is recursive.

#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum AnyNumber {
    UInt8(u8),
    UInt16(u16),
    UInt32(u32),
    ...
    Int64(i64),
}

impl Zero for AnyNumber {
    fn is_zero(&self) -> bool {
        match self {
            AnyNumber::UInt8(v) => v.is_zero(),
            AnyNumber::UInt16(v) => v.is_zero(),
            AnyNumber::UInt32(v) => v.is_zero(),
            ...
            AnyNumber::Int64(v) => v.is_zero(),
        }
    }
    fn zero() -> Self {
        Zero::zero()
    }
}
warning: function cannot return without recursing
  --> /Users/edmund/Programming-Local/tnc-analysis/lib/src/lib.rs:83:5
   |
83 |     fn zero() -> Self {
   |     ^^^^^^^^^^^^^^^^^ cannot return without recursing
84 |         Zero::zero()
   |         ------------ recursive call site
   |
   = help: a `loop` may express intention better if this is on purpose
   = note: `#[warn(unconditional_recursion)]` on by default

The app hangs when the code depends on Zero.

In general, I'm not sure how I would implement Zero for an enum...

You are indeed immediately and infinitely recursing.

You need to choose which variant to return.

    fn zero() -> Self {
        AnyNumber::UInt8(<_>::zero())
    }
1 Like

Because I can't find the variant, should I think about choosing a zero that can be cast to whatever is required when I need a zero? (i.e., for all the variants?)

Also, what ref is causing the recursion?

That's some question about your overall goal which I'm not in a position to answer.

I don't know what you mean by ref, but the compiler sees

impl Zero for AnyNumber {
    fn zero() -> Self {
        Zero::zero() // <---
    }
}

And thinks "hmm I need some implementor of Zero where Zero::zero returns AnyNumber, or if that doesn't work, something that can coerce to AnyNumber."

Given the signature of Zero::zero, it determines that it's looking for the implementation of Zero by AnyNumber, since that's a Zero::zero that returns AnyNumber.

It has thus determined that you want to call <AnyNumber as Zero>::zero (recursively).


On a higher level of reasoning, of course this can't do anything reasonable:

impl Zero for AnyNumber {
    fn zero() -> Self {
        Zero::zero()
    }
}

As you never specify which variant the output should be (and Rust isn't the type of language to arbitrarily choose one or have every type nullable, etc).

3 Likes

I am assuming you mean the trait from the num crate, i.e. num::Zero.

It is not immediatly clear to me if it would even be sensible to implement Zero for you enum. The documentation states:

Trait num::Zero

Defines an additive identity element for Self.

Laws

a + 0 = a       āˆ€ a ∈ Self
0 + a = a       āˆ€ a ∈ Self

Think of it like this. If you had a struct like

struct MyI32(i32);

then an implementation of Zero seems pretty straight forward. Which value of MyI32 would fulfill the laws stated above? Well it's MyI32(0). Of course only if you also implement Add and PartialEq for MyI32.

Zero could then be implemented as follows

impl Zero for MyI32 {
    fn is_zero(&self) -> bool {
        self.0 == 0
    }
    fn zero() -> Self {
        Self(0)
    }
}

This will now fulfill the laws for the additive identity.

let a = MyI32(<some i32>)
assert(MyI32::zero() + a == a);
assert(a + MyI32::zero() == a);

Again, given that you also implement Add and PartialEq in a sensible manner.

But for your enum, which Variant should be the zero?

It could be AnyNumber::Uint8(0) but it could also be AnyNumber::Uint16(0) or any of the other variants. So not knowing what exactly you wanna do or what operations AnyNumber supports (i.e. can they be added, multiplied, equated and how does it work with operations between variants?) I would guess that Zero is probably not a sensible thing to implement here.

Thank you for the explanation.

The type is used to expand a matrix with zeros, that ultimately do get used in a sum/add operation.

Depending on the variant used, I want to use the required zero so that the types match. So if I’m using u32, I fill it with u32 zeros; f64, 0.0 etc.

You could also consider adding Zero (and maybe One) as a fieldless enum-variant.

Consider whether you really want an enum though. The enum allows you to work with different types at runtime. But if the type can be chosen at compile-time, then generics may be an alternative.

1 Like

Maybe post an example of your code.
The question is too generic to give you tipps.

You can also look at the crate nalgebra and see how they implement their Matrix.

It is really unclear what you are wanting. There's nothing in the signature or the types that could possibly lead to a decision as to which variant should be returned.

Maybe what you really want is a generic Zero<T> trait parameterized on a type that contains some information as to the desired variant?

1 Like

If working with generics (instead of an enum), you might also want to take a look at num::PrimInt, which implies Zero.

That's a good point and something I did consider. I actually tried to use a generic but ran into the trait not being object safe (e.g., traits like Add use self; and by definition of what I'm doing the memory size will be different (unknown at compile time) without more indirection). I could not find a way around that hurdle; although I suspect there is a way.

When you are right, you are right. Barring some way to work-around this (e.g., "a la" using a parent trait to obviate the need for trait aliases), I'm actually starting to believe the implementation of Zero in num_traits made an unfortunate design decision by excluding a reference to self for the zero function (already required for is_zero, and by def of returning Self, already not object safe).

I get how zero is a way to produce a "neutral" value for the addition operation (the "identity element" in a monoid specification). The current design of Zero presumes there is only one identity element per type. However, it misses how a person might use an enum type to host multiple types, each with their own identity value.

Enums and traits are both ways to unify behavior across types. Has the design over-looked how an enum might need an identity element for each variant?

If I'm right, this would be the first time I see real "harm" (if you will :)), in how I see a strong Rust bias towards traits. Traits are great, but enums aren't like the "goto" syntax that should be avoided :))

The whole point of zero() is to construct a new "zero" value with the algebraic properties outlined above. Taking a self parameter in a function that's supposed to construct a new value without any inputs doesn't make any sense.

I am not at all clear on what you're actually trying to accomplish, perhaps a higher level explanation would get better answers.

1 Like

I get your point more than I did when I first started thinking about the problem. The caller needs to generate zero "from thin air".

Conceptually, I don't think how I'm thinking about it negates that motivation. The idea of "identity" does not happen in a void, but rather a type context that can be retrieved, and in my view required. My model would be "given I have a value/variant x, generate the "identity" value for x".

I don't know how, but "identity" should be able to align with variant. The current design is limited by requiring alignment with type.

Their data structures are generic quantified/bound by implementation of various traits. Zero is one of those traits :-/

My question would be: Do you need to have a dynamic type at runtime at all? If yes, an enum may be a reasonable choice.

Or is it possible to just entirely use generics (by adding type arguments <T: PrimInt> to all your functions and methods)?

I bumped into the limit of being generic when I needed to extract the underlying type. Specifically here:

fn numbers(input: &Series) -> Result<Box<dyn Iterator<Item = AnyNumber> + '_>> {
    match input.dtype() {
        DataType::UInt8 => Ok(Box::new(
            input.u8()?.into_no_null_iter().map(AnyNumber::UInt8),
        )),
        DataType::UInt16 => Ok(Box::new(
            input.u16()?.into_no_null_iter().map(AnyNumber::UInt16),
        )),
        DataType::UInt32 => Ok(Box::new(
            input.u32()?.into_no_null_iter().map(AnyNumber::UInt32),
        )),
        ...
        DataType::Float32 => Ok(Box::new(
            input.f32()?.into_no_null_iter().map(AnyNumber::Float32),
        )),
        DataType::Float64 => Ok(Box::new(
            input.f64()?.into_no_null_iter().map(AnyNumber::Float64),
        )),
        _ => Err(eyre!(
            "The underlying type is not a AnyNumber: {}",
            input.dtype()
        )),
    }
}

I did not know how to return AnyNumber as a trait without requiring object safety. Is there another layer of indirection I can use?

Note: Object safety isn't possible given the trait methods that return Self.

... actually, I see more now how it is a constraint that goes deeper than I have thus far understood. The choice to-date to avoid using self is the only way to go without requiring some sort of binary operation be included in the spec.

That's right, but that's because most number types are a single type, so your use case probably looks very much like a niche or an exception from the point of view of num-traits.

So are the entries of your matrix of type AnyNumber?
If yes isn't that a waste of space? If not, what does your Matrix type look like?