Various beginner questions (enums, from_str, overloading)

Hi, i am fairly new to Rust, and thought this would be a more appropriate place to ask various smaller questions in one post (compared to e.g. StackOverflow).

So I have written these 2 files:

// utils.rs
pub struct Date {
    pub num: u32,
}

pub enum TenorUnit {
    D,
    W,
    M,
    Y,
}

pub struct Tenor {
    pub count: i16,
    pub unit: TenorUnit,
}

impl Tenor {
    pub fn new(count: i16, unit: TenorUnit) -> Tenor {
        Tenor {
            count: count,
            unit: unit,
        }
    }
    pub fn from_str(s: &str) -> Tenor {
        let count = s[0..s.len() - 1].parse::<i16>().unwrap();
        let unit = match s.chars().last().unwrap() {
            'D' => TenorUnit::D,
            'W' => TenorUnit::W,
            'M' => TenorUnit::M,
            'Y' => TenorUnit::Y,
            _ => panic!("Invalid tenor unit ({})", s.chars().last().unwrap()),
        };
        Tenor {
            count: count,
            unit: unit,
        }
    }
}

pub trait TenorLike {
    fn to_tenor(self) -> Tenor;
}

impl TenorLike for Tenor {
    fn to_tenor(self) -> Tenor {
        self
    }
}

impl TenorLike for &str {
    fn to_tenor(self) -> Tenor {
        Tenor::from_str(self)
    }
}

impl Date {
    pub fn new(num: u32) -> Date {
        Date { num }
    }
    pub fn add<T: TenorLike>(&self, t: T) -> Self {
        let tenor = t.to_tenor();
        let new_num = match tenor.unit {
            TenorUnit::D => (self.num as i32 + tenor.count as i32) as u32,
            TenorUnit::W => (self.num as i32 + tenor.count as i32 * 7) as u32,
            TenorUnit::M => (self.num as i32 + tenor.count as i32 * 30) as u32,
            TenorUnit::Y => (self.num as i32 + tenor.count as i32 * 365) as u32,
        };
        Date { num: new_num }
    }
}

and

// main.rs
mod utils;
use crate::utils::Date;
use crate::utils::Tenor;
use crate::utils::TenorUnit;

fn main() {
    let d = Date::new(44_528);  // Excel format, 44528 equals 2021-11-28 
    let m1 = d.add(Tenor::new(3, TenorUnit::W));  // add 3 weeks
    let m2 = d.add(Tenor::new(2, TenorUnit::M));  // add 2 months
    let m3 = d.add(Tenor::new(1, TenorUnit::Y));  // add 1 year
    let m4 = d.add(Tenor::from_str("3M"));        // add 3 months

    for m in [m1, m2, m3, m4].iter() {
        println!("{}", m.num);
    }
}

My questions are:

  1. Are there ways to automatically convert enum values to strings, so that I don't have to manually write 'D' => TenorUnit::D, etc everywhere? I know e.g. C++ cannot do this without macros or external libraries. Does Rust offer something here within its standard?

  2. When returning the final Date in the Date::add function, what is "better" to write? Date{num:new_num} or Date::new(new_num)? Basically, I wonder if Date { } is faster as it creates the value "inline", whereas Date::new is slower as it requires a superfluous function call to new (which then just calls Date { } anyway)?

  3. In the Date::add function, there is a lot of casting in lines like this:
    (self.num as i32 + tenor.count as i32 * 7) as u32
    Is this the correct way to write this? Is it possible to reduce the number of times I need to cast?

  4. By using trait TenorLike I have tried to overload function Date::add, so that I can use Tenor inputs, as well as str inputs for convenience. Is there a more natural way to implement overloading (specifically, if the only second overload is a string)? Basically wondering if there is a way to remove boilerplate like trait TenorLike { fn to_tenor(self) -> Tenor; } ?

  5. Is the way I implemented function Tenor::from_str ok, or should one always implement the trait:
    impl FromStr for Tenor { fn from_str(s: &str) -> Result<...> { ... } } ?
    The reason I am hesitant to use the trait is that I'd have to add .unwrap() every time I use Tenor::from_str and add use std::str::FromStr; in my main.rs file, which adds an extra import & hence more boilerplate. And I feel from_str should be included in use crate::utils::Tenor; to begin with.

Apologies if my questions are naive. I am just trying to get the basics right. :slight_smile: Any other comments on my code are also appreciated.

For any struct/enum in Rust, you can derive the Debug trait. That'd give you some output, which is approximately what you want. Anything fancier you have to manually roll or use an external crate.

#[derive(Debug)]
enum TenorUnit {
    D,
    W,
    M,
    Y,
}

That said, you were right that the only "way" to automatically print enums is using a macro - in this case the std library has you covered.

It doesn't matter, from a performance POV. The compiler will inline almost everything it can and many more things than you and I can imagine. It is a matter of style. If you have a new function, use it. If you don't, then directly construct the struct.
When do you make a new function? The answer as usual is, it depends. If you have a struct that is exported (pub that is), but its fields aren't (which usually is the case), then you need compulsorily need a new function to construct the struct from another module. Sometimes, you need some additional processing before constructing the struct - then also a new function is a good idea.

Yes it is correct. And in Rust there is no real way to reduce the number of casts. You can write a function like this though:

fn offset(num: u32, cnt: i16, scale: i32) -> u32 {
    (num as i32 + scale * (cnt as i32)) as u32
}

And then set the scale appropriately for different TenorUnit.

Before I proceed with this point, I would like to point out one thing - there is no overloading in Rust and definitely nothing in the C++ sense of overloading. Functions in Rust are very similar to spirit as C, only having the huge benefit of generic. You have made you add function generic over all types T, where T satisfied a type constraint - namely T must implement the TenorLike trait.
Now, there is another common way of writing this add function - using the Into trait.

fn add<T: Into<Self>>(&self, rhs: T) -> Self {
    let tenor: Tenor = rhs.into();
    // Rest of the logic ...
}

So, as the docs suggest, you should implement the From trait and the compiler automatically provides an implementation for the Into trait.

impl From<&str> for Tenor {
    fn from(s: &str) -> Self {
        let count = s[0..s.len() - 1].parse::<i16>().unwrap();
        let unit = match s.chars().last().unwrap() {
            'D' => TenorUnit::D,
            'W' => TenorUnit::W,
            'M' => TenorUnit::M,
            'Y' => TenorUnit::Y,
            _ => panic!("Invalid tenor unit ({})", s.chars().last().unwrap()),
        };
        Self {
            count: count,
            unit: unit,
        }
    } 
}

Finally, you can call add simply like this let m4 = d.add("3M");.

FromStr is useful if you intend to call the parse method on an str and get a Tenor. Otherwise, you can implement a From<&str> or a TryFrom<&'str> for failable conversion (ie, returns a Result). Or you can leave it as is, if it is meant to be used as you have.

  • Please avoid pub fields in structs. 90% fields need just accessors if at all. In case a mutator is required, you can consider your choices of leaving it pub or adding the mutator method.
  • Please use descriptive names for you enum fields - like Day, Month, Week, Year. Self documentation is the best documentation (not kidding).
  • In general, it is a bad idea to panic in library functions on invalid input. Library users generally expect to get reasonable response on bad input - calling panic in a parsing function can often make it unusable (because if the user can check for parsing errors in the input before passing it to your function, then the user can also parse on their own, making your parse function mildly redundant).

There are no naive questions. :slightly_smiling_face:
I learnt about the FromStr trait from your question (and I have been using Rust actively for two years now).

For (1), there's strum.

Just don't worry about minute performance details like this, unless you measured that it matters. In the overwhelming majority of cases, it just won't matter.

Don't write code based on perceived performance characteristics. Write code that is the easiest to understand and change later.

No, not really. It contains a hidden panic if the conversion fails. This is bad style, because it doesn't give your users any chance to handle errors, and in conversion/parsing situations, errors are expected. There's a reason FromStr has the signature it has — use it.

3 Likes

I assume it would be Into<Tenor> ?


So I would change the Date class to pub struct Date { num: u32 } and add function pub fn num(&self) -> u32 { self.num }?


Thanks! :slight_smile:


So just to double-check: if a function can fail (or "throw") in Rust, then the correct way to implement this is to always return a Result<...> object from that function? So the throw-try-catch concept from C++ does not exist in Rust?

Ok, so as suggested, I tried let tenor: Tenor = t.into(); in the Date::add function together with implementing impl From<&str> for Tenor { fn from(s: &str) -> Self { ... } } . This works nicely, and I can now write d.add("3M").

Now, in order to do error-handling I removed the from(s: &str) -> Self function and instead added

impl TryFrom<&str> for Tenor {
    type Error = ();
    fn try_from(s: &str) -> Result<Self, Self::Error> {
        let count = s[0..s.len() - 1].parse::<i16>().unwrap();
        let unit = match s.chars().last().unwrap() {
            'D' => TenorUnit::D,
            'W' => TenorUnit::W,
            'M' => TenorUnit::M,
            'Y' => TenorUnit::Y,
            _ => return Err(()),
        };
        Ok(Tenor::new(count, unit))
    }
}

Then I changed

let tenor: Tenor = t.into();

to

let tenor: Tenor = t.try_into().unwrap();

But now the compiler is complaining:

12 |  let m4 = d.add("3M");
   |             ^^^ the trait `From<&str>` is not implemented for `Tenor`

How to fix this?

Self is an alias for Tenor here, so you can use them interchangeably.

You have mixed the implementation of two different traits: From and TryFrom. The first one is meant to be used for infallible transformations, and the second one for fallible ones. The same logic applies for the Into and TryInto traits.

Not "the correct" way (panicking isn't "incorrect"), but it's more idiomatic, and it should be preferred.

Well, not as-is, and not with this syntax. Panics can sometimes be caught, but that mechanism is not reliable (you can turn it off like you would do it using -fno-exceptions in C++).

Furthermore, panicking has the same problems as exceptions: they are not typed. Results can't just be ignored accidentally, so they lead to code that has a higher chance of being error-free.

1 Like

Replace the trait bound on add from <T: Into<Tenor>> to <T: TryInto<Tenor>>.

Function fn add<T: Into<Self>>(&self, rhs: T) -> Self is part of class Date, which makes Self=Date, doesn't it? Hence my question whether it should be Into<Tenor>.


So once TryFrom is implemented, how do I make use of it, if not via t.try_into().unwrap() ?


What is the syntax to catch panics? panic::catch_unwind(|| d.add("abcM")) ?

Thank you. I've tried

pub fn add<T: TryInto<Tenor>>(&self, t: T) -> Self {
    let tenor: Tenor = t.try_into().unwrap();
    // ...
}

But now the compiler says:

error[E0599]: the method `unwrap` exists for enum `Result<Tenor, <T as TryInto<Tenor>>::Error>`,
              but its trait bounds were not satisfied
   --> utils.rs:102:41
    |
102 |         let tenor: Tenor = t.try_into().unwrap();
    |                                         ^^^^^^ method cannot be called on `Result<Tenor, <T as TryInto<Tenor>>::Error>`
    |                                                due to unsatisfied trait bounds
    |
    = note: the following trait bounds were not satisfied:
            `<T as TryInto<Tenor>>::Error: Debug`

Sorry for these questions.

What is the error type for the TryFrom implementation? For that type you must implement Debug. In general, you can use the derive macro to implement Debug - #[derive(Debug)].

Yes, that is correct.

You can avoid all casts by changing the types of num and count to i32.

I think it's more that Error results can be handled in a flexible way by code, whereas with a panic, it's usually a case of reporting the error (to a human) somehow, and either aborting execution entirely, or recovering to some known safe state. If that's ok, I think panic can be a perfectly good solution.

Well, it's definitely not "more". The point is that Result is typed whereas panics aren't.

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.