4

I'm trying to understand how to implement a generic trait in Rust.

While I've seen a number of examples, the examples are too tied to a specific use (e.g. genomic mutators) for me to be able to understand at this point in my Rust development.

Instead, here's a simple example based on something fairly universal--incrementing:

trait Incrementable {
    fn post_inc(&mut self) -> Self;
    fn post_inc_by(&mut self, n: usize) -> Self;
}

impl Incrementable for usize {
    fn post_inc(&mut self) -> Self {
        let tmp = *self;
        *self += 1;
        tmp
    }

    //"Overload" for full generalizability
    fn post_inc_by(&mut self, n: usize) -> Self {
        let tmp = *self;
        *self += n;
        tmp
    }
}

fn main() {
    let mut result = 0;
    assert!(result.post_inc() == 0);
    assert!(result == 1);

    assert!(result.post_inc_by(3) == 1);
    assert!(result == 4);
}

The above code works, but is lacking because it isn't generalizable to all numeric types without writing a lot of boilerplate code.

In my attempts to generalize the above code, I've gotten into fights with the type system, borrow checker or been forced down a path of implementing FromPrimitive for every type I want to support in my generic version (effectively putting me back to square one).

Can you help me understand how to implement Incrementable generically, such that post_inc() and post_inc_by() work for at least all integer and float types, ideally without having to write an implementation for each type?

I am hoping the answer will help me see how traits, implementations, types and associated types can work together in a more straightforward use case than I've been able to come across.

I'm on Rust 1.16.0.

2
  • 2
    I typed this up on my tablet on the train on my way home from work. Without a proper setup to Google further thats all I could come up with.. but unfortunately it requires not only the nightly compiler but a deprecated trait since 1.11. Not a great example for you perhaps but shows one way I guess. Commented Jan 16, 2017 at 5:37
  • Thank you @SimonWhitehead. You're right about not wanting to use deprecated features, but it is still educational to see how you put this together. For the moment Self::one() does indeed work. Ultimately I want to add a more flexible method post_inc_by(n: usize) to increment by n rather than just 1, though, so if you have any ideas on that I would be grateful. (I've added this to the question.) I'll work through the implementation you've provided, though and see if I can use non-deprecated equivalents. Thank you! Commented Jan 16, 2017 at 5:50

4 Answers 4

5

@Simon Whitehead's example can easily be adapted for stable Rust:

trait Incrementable: Copy + std::ops::AddAssign<Self> {
    fn one() -> Self;

    fn post_inc(&mut self) -> Self {
        self.post_inc_by(Self::one())
    }

    fn post_inc_by(&mut self, n: Self) -> Self {
        let tmp = *self;
        *self += n;
        tmp
    }
}

impl Incrementable for u8  { fn one() -> Self {1} }
impl Incrementable for u16 { fn one() -> Self {1} }
impl Incrementable for u32 { fn one() -> Self {1} }
impl Incrementable for u64 { fn one() -> Self {1} }
impl Incrementable for i8  { fn one() -> Self {1} }
impl Incrementable for i16 { fn one() -> Self {1} }
impl Incrementable for i32 { fn one() -> Self {1} }
impl Incrementable for i64 { fn one() -> Self {1} }
impl Incrementable for f32 { fn one() -> Self {1.0} }
impl Incrementable for f64 { fn one() -> Self {1.0} }

While you need to do the implementation for each type, each of them is extremely simple.

You can also use a macro to hide repetitive implementations:

macro_rules! impl_Incrementable{
    ($($m:ty),*) => {$( impl Incrementable for $m  { fn one() -> Self { 1 as $m } })*}
}

impl_Incrementable!{u8, u16, u32, u64, i8, i16, i32, i64, f32, f64}
Sign up to request clarification or add additional context in comments.

8 Comments

Nice! I considered this shortly after posting my comment but ... typing code on an Android tablet that is about 4 years old is a terrible terrible experience :D
You don't need Self::one(), self.post_inc_by(1 as Self) would work just as well.
@ljedrz the complier complains when I change Self::one() to 1 as Self with "error: non-scalar cast: 'i32' as 'Self'"'. Ideas?
@aSpex This is a nice solution, especially the macro for eliminating the need to write all those type-specific implementations, thank you. Can you explain why in main(), I can write post_inc_by(3), but in post_inc(), writing post_inc_by(1) gives expected Self, found integral variable compile error?
@bRadGibson Sorry does not work for i8. This is playground link if you interested: play.rust-lang.org/…
|
3

The types that we can increment need to

  1. know the operator and += (AddAssign)
  2. define a value for the "one"-element
  3. be copyable as we want to keep the old un-incremented value.

Point 1. and 3. we can assure by using a trait bound, for point 2. we can set up a trait that has the function one() -> self.

So here is a working example:

// We need to know the operator "+="
use std::ops::AddAssign;

// The trait needs a type parameter
trait Incrementable<T> {
    fn post_inc(&mut self) -> Self;
    fn post_inc_by(&mut self, n: T) -> Self;
}

// We need a trait which tells us the "one" value for a type
trait Increment {
    fn one() -> Self;
}

// We need to implement the Increment trait for every type
// we want to increment.
impl Increment for usize {
    fn one() -> usize {
        1
    }
}

// Finally we implement the Increment trait generically for all types that
// * know the operator "+=" AddAssign
// * are copyable
// * implement our Increment trait, so that we know their "one" value
impl<T: AddAssign + Increment + Copy> Incrementable<T> for T {
    fn post_inc(&mut self) -> Self {
        let tmp = *self;
        *self += T::one();
        tmp
    }

    //"Overload" for full generalizability
    fn post_inc_by(&mut self, n: T) -> Self {
        let tmp = *self;
        *self += n;
        tmp
    }
}

fn main() {
    let mut result = 0;
    assert!(result.post_inc() == 0);
    assert!(result == 1);

    assert!(result.post_inc_by(3) == 1);
    assert!(result == 4);
}

You don't have to write an implementation of Incrementable for each type, but you do have to implement the trait that supplies the one() function. You can't get away without that, because for non numerical types it is not obvious what "increment by one" means.

I kept everything in a generic implementation that can be implemented generically. The exception is the T::one(), so no boiler-plate code needed except this one trivial function for each type.

4 Comments

@JohannesMueller: +1 I appreciate the clear step-by-step breakdown, especially why the Copy trait is needed.
@bRadGibson FWIW, I think this answer has the best explanation of why you have to implement the trait multiple times, but this answer has the nicer implementation.
Excellent answer, shows that there's nothing really magical in the num crate (for this simple purpose). Also, consider using Clone in preference to Copy. It only requires the explicit call to clone, e.g. let tmp = self.clone(), and instantly buys the support for arbitrary-width types, such as BigInt. The primitive types will still work because Copy automatically implies Clone.
Nice. Note that bigint and biguint also don't implement AddAssign or SubAssign, so there would be a bit more work to get them into the family. Still, I like the idea and will explore it further. Thank you!
3

You could do this with macros, following what the std did:

trait Incrementable {
    fn post_inc(&mut self) -> Self;
    fn post_inc_by(&mut self, n: Self) -> Self;
}

macro_rules! post_inc_impl {
    ($($t:ty)*) => ($(
        impl Incrementable for $t {
            fn post_inc(&mut self) -> Self {
                self.post_inc_by(1 as Self)
            }

            fn post_inc_by(&mut self, n: Self) -> Self {
                let tmp = *self;
                *self += n;
                tmp
            }
        }
    )*)
}

post_inc_impl! { usize u8 u16 u32 u64 isize i8 i16 i32 i64 f32 f64 }

fn main() {
    let mut result = 0;
    assert!(result.post_inc() == 0);
    assert!(result == 1);

    assert!(result.post_inc_by(3) == 1);
    assert!(result == 4);
}

It is possible without macros if you use the num crate:

extern crate num;

use num::Num;

trait Incrementable<T: Num> {
    fn post_inc(&mut self) -> Self;
    fn post_inc_by(&mut self, n: T) -> Self;
}

impl<T: Num + std::ops::AddAssign<T> + Copy> Incrementable<T> for T {
    fn post_inc(&mut self) -> T {
        let tmp = *self;
        *self += T::one();
        tmp
    }

    fn post_inc_by(&mut self, n: T) -> Self {
        let tmp = *self;
        *self += n;
        tmp
    }
}

4 Comments

+1 for the reference to std implementation. That is a good idea to look there to see how similar problems have been solved.
@bRadGibson I'm not even good with macros, but plenty of things are implemented with them in the std - and if it does it like that, it usually means it's a good idea :).
Either it's a good idea, or it's just the best way we have currently ;-)
Wow--very nice, @ljedrz. The solution you came up with using the num crate is what I was looking for.
1

I know this is quite old, but I think there is a cleaner solution (maybe it was not working at the time of the original post).

trait Incrementable {
    fn post_inc(&mut self) -> Self;
    fn post_inc_by(&mut self, n: Self) -> Self;
}

impl<T> Incrementable for T where 
    T: Copy + std::ops::AddAssign<Self> + From<bool> // <-
{
    fn post_inc(&mut self) -> Self {
        self.post_inc_by(Self::from(true)) // <- 
    }

    fn post_inc_by(&mut self, n: Self) -> Self {
        let tmp = *self;
        *self += n;
        tmp
    }
}

The trick here is that all numeric types seem to be From<bool>, and that true is actually 1.

Other solutions rely on macros to generate all the one()s, leverage third-party crates, or fail to work with i8 if using some kind of From<u8> when incrementing (or the inverse). With the proposed approach the only problem I see is that from(true)=1 is documented but not very explicit.

Some tests:

fn test_inc() {
    let mut x = 42i8; x.post_inc(); println!("{x}"); // 43
    let mut x = 42u32; x.post_inc(); println!("{x}"); // 43
    let mut x = 42usize; x.post_inc(); println!("{x}"); // 43
    let mut x = 4.2f64; x.post_inc(); println!("{x}"); // 5.2
    //...
}

1 Comment

Agreed. Today, I would use the num crate and impl Incrementable for T where T: num::PrimInt or equivalent.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.