0

I'm teaching Rust, and so I go to adventofcode (parallel with boring reading rustbook).

And in one task I need to track direction, during moving and rotations such as "Straight / Left / Back / Right" && "Straight + Right + Right == Back".

*) suppose for a second, that "absolute direction" and "relative direction" are represented as single type.

So I trying to solve this issue using enums. And during this I need two things:

  1. Contiguosly place discriminants from "integer 0" to last
  2. Get discriminants count (or "enum size") either straightforward or using some tricks.

But I can't do the second thing.

enum Dir {
    Up,
    Left,
    Down,
    Right,
    //Last = (Dir::Right) as isize,
} 
impl Dir {
    fn add_dir(&self, add_dir: &Self) -> Self {       
        let last = Dir::from_int((self.to_int() + add_dir.to_int) / (Dir::Last.to_int() + 1));
        return last;
    }
}

So the questoin is: how can I implement domain-specific, i.e. type-safe, modulo arithmetic in rust (with no performance requirements if it's important).

======= Updated: The local and narrow question is - how to get constant "4" (modulo for modular arithmetic), but don't type it as literal (the last thing, typing constants as literal - is a very errorfull way when programmer starts to change his code).

  • I don't quite understand your problem, but there are the [`std::ops::Rem`](https://doc.rust-lang.org/std/ops/trait.Rem.html) and [`std::ops::RemAssign`](https://doc.rust-lang.org/std/ops/trait.RemAssign.html) traits you can implement for your enum if you want to implement the `%` operator for your enum. Is this what you are looking for? (In general [`std::ops`](https://doc.rust-lang.org/std/ops/index.html#) contains traits that allow you to overload (most of -- some traits are nightly) Rust's operators) – Jonas Fassbender Mar 21 '23 at 16:14
  • my problem is - how to properly get constant 4 from enum. Butnot write it as literal in code (this is very errorfull way when programmer will change his code). – Konstantin Kazartsev Mar 21 '23 at 16:52

2 Answers2

2

Get discriminants count (or "enum size") either straightforward or using some tricks.

my problem is - how to properly get constant 4 from enum. Butnot write it as literal in code (this is very errorfull way when programmer will change his code).

You can reliably get the number of enum variants using the strum crate's EnumCount, which adds the Dir::COUNT static usize value to your enum:

/*
[dependencies]
strum = { version = "0.24", features = ["derive"] }
*/
use strum::EnumCount;

#[derive(EnumCount)]
enum Dir {
    Up,
    Left,
    Down,
    Right,
} 

fn main() {
    assert_eq!(Dir::COUNT, 4);
}

Rustexplorer


Contiguously place discriminants from "integer 0" to last

Rust does so for variants implicitly. See @Sven Marnach's answer.

Jonas Fassbender
  • 2,371
  • 1
  • 3
  • 19
  • Thank you! And some related question: how can I navigate trough crates.io? It looks impossible (or at least very very unlikely) for me to find strum && EnumCount in myself. – Konstantin Kazartsev Mar 21 '23 at 18:02
  • @KonstantinKazartsev If I only knew. I struggle with finding crates myself. I've written several crates that expose functionality that has already been implemented in other crates I simply didn't know existed, even though I searched for keywords I thought were very clear. Most helpful crates in the likes of `strum` I discovered by reading questions like yours. GitHub also has pretty useful recommendations based on the stars I gave to the crates I use. – Jonas Fassbender Mar 21 '23 at 19:33
1

Enums without any data on its variants can be easily converted into integers. The first variant will have discriminant 0, the second 1 and so on. The conversion can be performed with, e.g., dir as u8, where dir has type Dir.

The conversion in the other direction is less straight-forward. Since not all integers are valid discriminants, the conversion is fallible. There are crates helping to implement this conversion automatically (e.g. num_enum, but for this simple case I recommend implementing the conversion yourself. You can use the built-in TryFrom trait for the fallible conversion, and the built-in Add trait for the addition:

use anyhow::{Error, Result};
use std::ops::Add;

#[derive(Copy, Clone, Debug)]
enum Dir {
    Up,
    Left,
    Down,
    Right,
}

impl TryFrom<u8> for Dir {
    type Error = Error;

    fn try_from(value: u8) -> Result<Self> {
        let dir = match value {
            0 => Dir::Up,
            1 => Dir::Left,
            2 => Dir::Down,
            3 => Dir::Right,
            _ => return Err(Error::msg("direction out of bounds")),
        };
        return Ok(dir);
    }
}

impl Add<Dir> for Dir {
    type Output = Dir;

    fn add(self, other: Dir) -> Self {
        ((self as u8 + other as u8) % 4).try_into().unwrap()
    }
}

fn main() {
    dbg!(Dir::Left + Dir::Down);
}

Playground

I've used anyhow::Error as the conversion error, but of course you can use whatever custom error type you prefer. The implementation of addition uses % 4 to make sure the result isn't out of range, so using unwrap() on the result of the conversion is fine.

The implementation of the conversion is a bit verbose, but for only four variants I wouldn't mind it, since it's obvious on a glance what it does. For more complex cases (or even for this one) you may prefer one of the crates that automate this.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • Excuse me my question is: how to get constant "4" from enum properly, but not type it as a literal in code (this is very errorfull way, when you will change your code). – Konstantin Kazartsev Mar 21 '23 at 16:56
  • Thank you. And I have other question : why discriminant-enums (enums which consists only from discriminants) is not implement trait "Copy" automatically? So now I have to derive Copy explicitly, and what if I add struct into enum lately? It'll impact performance very badly. – Konstantin Kazartsev Mar 21 '23 at 21:36
  • @KonstantinKazartsev For an enum that you expect to change or amend in the future, you should certainly not hardcode the number 4, and instead use one of the crates providing you the number of variants. However, in this particular chance, it is clear that you won't need to ever add further variants, so I'd personally simply hardcode the number. Of course this is a matter of preference. – Sven Marnach Mar 22 '23 at 10:41
  • Regarding the `Copy` trait, it usually doesn't have any effect on performance whatsoever. Even without the trait, objects are copied bitwise when being "moved", and the chance of the compiler eliding such copies is about the same. The main effect of the `Copy` trait is the different semantics – whether the original object afer a "move" can still be used or not. This is also the reason why it is not automatically implemented. If it were, you wouldn't be able to un-implmenent it if you desire the semantics of a non-`Copy` type. – Sven Marnach Mar 22 '23 at 10:47
  • Thank you for you carefull explanations. And I didn't understand one another detail: how "dir as u8" connects to "impl TryFrom Dir". I don't use std::convert::TryFrom so how does it works? Can you explain please – Konstantin Kazartsev Mar 22 '23 at 16:55
  • And more questions. You use "anyhow" package. Does usinng anyhow - is a standard way to bring Result with monadic behaviour in your application-level code (I mean for manadic behaviour you should use some particular ErrorType, and convert in\from another ErrorTypes and so on...)? I.e. anyhow::Error && anyhow::Result - standard user-code Error and user-code Result, if you want to use Result in a fast way? – Konstantin Kazartsev Mar 22 '23 at 17:48
  • @KonstantinKazartsev You can [convert any enum without data attached to the variants to a primitive integer type using `as`](https://stackoverflow.com/questions/31358826/how-do-i-convert-an-enum-reference-to-a-number). This works out of the box, and is unrelated to the `TryFrom` implementation. `TryFrom` is the standard trait for fallible conversions, and allows you to use `Dir::try_from(some_int)` or `some_int.try_into()` to convert to a `Dir` instance. – Sven Marnach Mar 23 '23 at 10:25
  • I don't quite understand your question about `anyhow`, but my recommendation is that you should use it for application code, as long as the application doesn't get too complex. For libraries that are intended to be used by others, I recommend custom error types instead. The `thiserror` crate simplifies implementing custom error types. – Sven Marnach Mar 23 '23 at 10:28
  • I could be not quite straitforward, try to reformulate: 1. I don't type "use std::convert::TryFrom" - but TryFrom trait was in module namespase - how" ? 2. There are "use std::error::Error ... Result >" vs "use anyhow {Result, Error} anyhow::Result". You use second variant - is this "common" for Rustatiance? Why? – Konstantin Kazartsev Mar 23 '23 at 10:35
  • @KonstantinKazartsev `TryFrom` is in the [standard prelude](https://doc.rust-lang.org/std/prelude/index.html), so it's injected into each namespace by module (unless you use the `#[no_prelude]` module attribute). – Sven Marnach Mar 23 '23 at 12:35
  • Whether to `use anyhow::Error` or explicitly qualifying it wherever it is used is a matter of preference. Do whatever you prefer. – Sven Marnach Mar 23 '23 at 12:36