Is it possible to tell to compiler "hey if reach this, do not compile"?

Hi Folks, I'm a newby to rust and I'm very excited to learn about it, specially about avoid runtime errors! I have a C, C++, Java and Python as background, and was playing around with enums and traits and reach to this.

Suppose that I have an Enum to represent two variants and implement Add trait to this, but I want to sum just variants of same type, and variants of different types should not be summed. In fact, different variants sum should not even allowed to compile the code. Is it possible to tell the compiler "hey check this staticaly, and if so, don't compile and warn user about it"?

enum MyEnum {
  A(i8),
  B(i8)
}

impl Add for MyEnum {
  type Output = Self;
  fn add(self, other: MyEnum) -> Self {
    match (&self, other) {
      (MyEnum::A(x),MyEnum::A(y)) => MyEnum::A(x+y),
      (MyEnum::B(x),MyEnum::B(y)) => MyEnum::B(x+y),
      _ => *self //but what I really want is in this case compiler should highlitgh an error
    }
  }
}

fn main() {
  let var = MyEnum::A(10) + MyEnum::B(5); //I want to know if it's possible to compile to avoid this staticaly
}

I came to this because as MyEnum variants increase, match arms will increase by 2^N. And why not to handle with Result<MyEnum, Error> or to panic! ? Well, if compiler can check static variable definitions, why not to static check this operation with different tyes and avoid a runtime break?

Please help me to achieve this goal! :smiley:

@Edit:
I clarify the situation and the example case in here:

When you want guaranteed static checking of some property, you have to ask the compiler for it — by using types and traits, not values or panics. That is:

Make A and B different types, not variants of one enum type.

If there are cases where you need to choose at run time, you can also have an enum that contains the A and B types. Or contains collections of them. There are a lot of possibilities, but we’ll need more context to recommend one.

16 Likes

enums can fundamentally represent any of their variants, so validating that a given variable holds a specific variant is something that requires deeper analysis. It's the kind of thing the compiler could do in an optimization pass, but I don't know of a way to make it illegal at compile time.

One option is to use a trait and concrete types.

Hi kpreid, thanks for the quickly answer. Let me elaborate better with an practical example.

If I had to abstract a Geometric 2D Space with a enum to the Axis, and a dot in space as a struct

enum Axis {
  X(i8),
  Y(i8)
}
struct Dot {
  x: Axis,
  y: Axis
}

If I implement Add traits to Axis and Dot, and Index trait to Dot, with those cases above, I can perform:
Axis::X + Axis::X, Axis::Y + Axis::Y, Dot + Axis::X (increasing Dot::x) and Dot + Axis::Y (increasing Dot::y).

Thinking in Algebra context, add Axis::X + Axis::Y would make a Dot, not an Axis. But as a said in previous example, I don't want to return this Dot in 2D space, I just want to this be an "impossible" case, and want to check this statically not at runtime.

(Considering a N-Space, Axis variant rising lead to a 2^N exhaustive match)

Is it possible?

Luckily the Add trait supports an optional Rhs type.

So, if you make two different types for X and Y, not one enum, is that not sufficient? What do you want to do that needs an enum?

Would be nice if you could define trait impls as const, then you might be able to figure out a way to do this with const assert?

This 3-Space case is just one example that lead the sitation that I want to know if its possible. But other logics, for other cases could lead to the same question:

"Wow, can I verify and forbidden this at compile time with some flag to compiler?"

In this specific case, implement all combination of match arms in Add() trait lead to a 2^N arms. But would be possible implement just N cases, and then forbidden the others at compile time?

This is main.rs file

mod example;
use example::{Axis,Dot};

fn main() {
    let mut myDot = Dot::new();
    
    let mut myX = Axis::X(10);
    let myY = Axis::Y(1);
    let myZ = Axis::Z(22);

    myX = myX + Axis::X(10); //this work as expected
    println!("{:?}",&myX); 
    

    myX = myX + Axis::Y(10); //Instead of panic or verify in runtime with Result<T,E>, I want to forbidden this sum at compile time!
    println!("{:?}",&myX);

    //This doesn't work as expected, because (Axis::X + Axis::Y) would do nothing (returning X)
    // and it results + Z also do nothing, just returning X
    let other_axis = Axis::X(10) + Axis::Y(10);  //Instead of panic or verify in runtime with Result<T,E>, I want to forbidden this sum at compile time!
    println!("{:?}",other_axis);

    myDot = myDot + myX + myY + myZ; //This work as expected, as Dot + Axis result in a Dot with that axis
    println!("{:?}",&myDot);

    //And I also want to recovery information from my Dot with Index in a Enum like
    println!("{:?}",&myDot[Axis::X(0)]);
    println!("{:?}",&myDot[Axis::Y(0)]);
    println!("{:?}",&myDot[Axis::Z(0)]);
}

This is example.rs file

use std::ops::{Add, Index};
type Primitive = i8;

#[derive(Debug)]
pub enum Axis {
    X(Primitive),
    Y(Primitive),
    Z(Primitive)
}

#[derive(Debug)]
pub struct Dot {
    x: Axis,
    y: Axis,
    z: Axis
}

impl Index<Axis> for Dot {
    type Output = Axis;
    fn index(&self, axis: Axis) -> &Self::Output {
        match axis {
            Axis::X(_) => &self.x,
            Axis::Y(_) => &self.y,
            Axis::Z(_) => &self.z,
        }
    }
}

//Implement sum axis of same variant
impl Add<Axis> for Axis {
    type Output = Self;
    fn add(mut self, rhs: Axis) -> Self {
        match (&self, rhs) {
            (Axis::X(v1),Axis::X(v2)) => Axis::X(v1+v2),
            (Axis::Y(v1),Axis::Y(v2)) => Axis::Y(v1+v2), 
            (Axis::Z(v1),Axis::Z(v2)) => Axis::Z(v1+v2),
            // Line below handle all the other combinations that are possible to the number of axis
            // fyi: (2^N - N) cases
            // but instead of return self, if dev in some part of their code try to sum Axis::X with Axis::Y (for example),
            // it should highligh as "hey you can't sum this".
            _ => self //I don't want to panic! this case :(
        }
    }
}

impl Add<Axis> for Dot {
    type Output = Self;
    fn add(mut self, axis: Axis) -> Self {
        match axis {
            Axis::X(v) => { self.x = self.x + Axis::X(v) },
            Axis::Y(v) => { self.y = self.y + Axis::Y(v) },
            Axis::Z(v) => { self.z = self.z + Axis::Z(v) },
        }
        self
    }
}

impl Dot {
    pub fn new() -> Self {
        Self {
            x: Axis::X(0),
            y: Axis::Y(0),
            z: Axis::Z(0)
        }
    }
}

Here is the solution everybody has been trying to suggest to you:

struct X(i8);
struct Y(i8);
struct Dot {
    x: X,
    y: Y,
}

enum Axis {
    X,
    Y,
}

impl Add for X {
    type Output = X;
    fn add(self, other: X) -> X {
        X(self.0 + other.0)
    }
}
impl Add for Y {
    type Output = Y;
    fn add(self, other: Y) -> Y {
        Y(self.0 + other.0)
    }
}
impl Add<Y> for X {
    type Output = Dot;
    fn add(self, y: Y) -> Dot {
        Dot { x: self, y }
    }
}
impl Index<Axis> for Dot {
    type Output = i8;
    fn index(&self, axis: Axis) -> &i8 {
        match axis {
            Axis::X => &self.x.0,
            Axis::Y => &self.y.0,
        }
    }
}

4 Likes

To let the compiler know that you claim to consider all cases, just leave out the _ case:

    match (&self, other) {
      (MyEnum::A(x),MyEnum::A(y)) => MyEnum::A(x+y),
      (MyEnum::B(x),MyEnum::B(y)) => MyEnum::B(x+y),
    }

But in this case, the compiler rightfully errors, because there are possible inputs that aren't covered. The correct solution here, is, as others have pointed out, to use different types.

This is exactly what a type system is for. But what that means is that you can only say it for things you can phrase in terms of types.

So there's always a potential runtime error once at the beginning when you're taking something that's not limited and using it to build the specific type. Then after that you don't have to handle the impossible cases at all, because they're not representable in the type.

The classic post about this is Parse, don’t validate or for a more Rust-specific take there's Aiming for correctness with types .

(But you can't have a compiler error for "don't compile if the user entered a 0 at runtime", because obviously the compiler can't keep a human from typing whatever. There's no way to say "fail to compile if the option isn't None" -- the way to say that is to give it type T instead of Option<T> in the first place.)

12 Likes

panic!() in a const fn can cause a compilation error, but that would have to be used in some context that forces the compiler to evaluate the function at compile time.

Currently Add::add can't be evaluated at compile time at all (const fn is not supported in traits). Even if it was supported, there's nothing that would force the compiler to always evaluate it, and there can be lots of cases where knowing values of the enums at compile time is proven to be impossible to know.

2 Likes