3

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.

Community
  • 1
  • 1
U007D
  • 5,772
  • 2
  • 38
  • 44
  • 2
    [I typed this up on my tablet on the train on my way home from work](https://play.rust-lang.org/?gist=71f451a4a04d61ad613806fd76a2cc96&version=nightly&backtrace=0). 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. – Simon Whitehead Jan 16 '17 at 05: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! – U007D Jan 16 '17 at 05:50

3 Answers3

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}
aSpex
  • 4,790
  • 14
  • 25
  • 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 – Simon Whitehead Jan 16 '17 at 10:16
  • 1
    You don't need `Self::one()`, `self.post_inc_by(1 as Self)` would work just as well. – ljedrz Jan 16 '17 at 10:17
  • @ljedrz the complier complains when I change `Self::one()` to `1 as Self` with "error: non-scalar cast: 'i32' as 'Self'"'. Ideas? – U007D Jan 16 '17 at 15:06
  • @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? – U007D Jan 16 '17 at 15:19
  • @bRadGibson this works when you implement it within a macro. I'll undelete my similar answer so you can see it: [answer](http://stackoverflow.com/a/41671656/1870153). – ljedrz Jan 16 '17 at 15:24
  • @ljedrz ah--I tried using it in post_inc(). Unhappy compiler. Yes, I see aSpex used it in the macro solution above. – U007D Jan 16 '17 at 15:33
  • @bRadGibson There is no implicit type casting in Rust. So the compiler won't convert `1` to the `Self`. If you try `self.post_inc_by(1 as Self)` it complains `non-scalar cast`. `Self` can be any type which impl `Copy` and `AddAssign`. There is no way to bound `Self` so that `1 as Self` always been valid. – aSpex Jan 17 '17 at 05:47
  • 1
    @bRadGibson Sorry does not work for `i8`. This is playground link if you interested: https://play.rust-lang.org/?gist=e39a122fe31a006afa23ecc1b63a19ae&version=stable&backtrace=0 – aSpex Jan 17 '17 at 10:12
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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • @JohannesMueller: +1 I appreciate the clear step-by-step breakdown, especially why the Copy trait is needed. – U007D Jan 16 '17 at 15:24
  • @bRadGibson FWIW, I think this answer has the best *explanation* of why you have to implement the trait multiple times, but [this answer](http://stackoverflow.com/a/41671697/155423) has the nicer implementation. – Shepmaster Jan 16 '17 at 15:47
  • 1
    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`](http://rust-num.github.io/num/num_bigint/struct.BigInt.html). The primitive types will still work because `Copy` automatically implies `Clone`. – user4815162342 Jan 16 '17 at 16:25
  • 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! – U007D Jan 17 '17 at 17:04
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
    }
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
ljedrz
  • 20,316
  • 4
  • 69
  • 97
  • 2
    +1 for the reference to std implementation. That is a good idea to look there to see how similar problems have been solved. – U007D Jan 16 '17 at 15:35
  • 2
    @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 :). – ljedrz Jan 16 '17 at 15:37
  • 2
    Either it's a good idea, or it's just the best way we have currently ;-) – Shepmaster Jan 16 '17 at 15:38
  • 1
    Wow--very nice, @ljedrz. The solution you came up with using the `num` crate is what I was looking for. – U007D Jan 16 '17 at 15:52