Traits vs methods (Messier book)

I am reading Ric Messier's book Beginning Rust Programming (Wrox) and I have noticed that in his examples, simple methods are never used. Instead a trait is defined, and then implemented (once). What I mean is that where I would have written

struct Foo {
    a: usize,
}

impl Foo {
    fn double(&mut self) {
        self.a *= 2;
    }
}

instead he would write something like

struct Foo {
    a: usize,
}

trait Doublable {
    fn double(&mut self);
}

impl Doublable for Foo {
    fn double(&mut self) {
        self.a *= 2;
    }
}

This style is used throughout the whole book. It looks needlessly verbose to me, and it also defines names that are rather useless. But I was wondering if there is some reason for this choice. Any idea?

Yes. Use traits only to represent common functionality, or some other cool tricks. Not like this.

Yes.

To make your book different? I dunno, ask the author.

1 Like

In other languages it's common to "program against an interface, not against an implementation", which is used to achieve loosely-coupled code. In Rust it's not so common, because it's very verbose and there's no easy way to mock an implementation for test purposes.

2 Likes

I agree. This feels more like something you'd see in older-style OO codebases, and isn't very idiomatic Rust.

The approach reminds me of an article Matklad posted back in 2020:

1 Like

Well, I pulled up the sample of chapter 1 to look, and it does have functions that aren't methods (albeit not inherents either).

But it also has

fn census(_world: [[u8; 75]; 75]) -> u16

as the first function definition, which makes me skeptical of the whole book.

In rust the only time you should name a parameter (or other local variable) starting with an underscore is when it's unused—which _world is not—in order to suppress the unused variables lint. Thus I'm worried that this had insufficient technical review.

It looks like the kind of thing you'd get if you started from another book that used the "member variables start with an underscore" naming convention, and just removed the static class part. That could also explain the pointless traits, if it was copied from inspired by a book that used interfaces like that.

Honestly, from looking at the first code example I'd say to skip the book and get a different one. While formatting isn't essential, when the very first example isn't consistent between i<74 and count <2, I get worried. Not to mention that despite a 1st-edition publish date of 2021, it's still using extern crate rand; instead of something 2018-edition-style like use rand::random;. (EDIT: Fixed, thanks!)

Blandy, Orendorff, & Tindall is highly regarded if you're looking for something different from The Book.

9 Likes

Generally it's frowned upon to use traits which are only implemented once. However, it allows you to later provide different data types which implement the same trait. So I see a bit of forward compatibility here.

This comes at a price because traits must be imported for their methods to be accessible:

pub mod m {
    pub struct Foo {
        pub a: usize,
    }
    pub trait Doublable {
        fn double(&mut self);
    }
    impl Doublable for Foo {
        fn double(&mut self) {
            self.a *= 2;
        }
    }
    pub mod unnameable_traits {
        pub use super::Doublable as _;
    }
}

// We need one of the following lines for the following function to compile:

//use m::*; // pollutes namespace
//use m::Doublable; // pollutes namespace a bit perhaps?
//use m::Doublable as _; // doesn't pollute namespace but is troublesome if there are more traits like that
//use m::unnameable_traits::*; // requires `m` to provide such a submodule

fn main() {
    let mut foo = m::Foo { a: 7 };
    foo.double();
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0599]: no method named `double` found for struct `Foo` in the current scope
  --> src/main.rs:27:9
   |
2  |     pub struct Foo {
   |     -------------- method `double` not found for this struct
...
6  |         fn double(&mut self);
   |            ------ the method is available for `Foo` here
...
27 |     foo.double();
   |         ^^^^^^ method not found in `Foo`
   |
   = help: items from traits can only be used if the trait is in scope
help: the following trait is implemented but not in scope; perhaps add a `use` for it:
   |
1  | use crate::m::Doublable;
   |

For more information about this error, try `rustc --explain E0599`.
error: could not compile `playground` due to previous error

My personal opinion is that in theory it would be cleaner to implement all methods through traits. But Rust isn't made for that and – apart from the more verbose syntax – it can be a hassle regarding imports and bringing methods into scope.

1 Like

use rand; is also to be avoided — it does nothing on any edition (other than fail if the name isn't already in scope).

3 Likes

After fighting such “forward compatibility” for two decades I beg you: find another book.

Maybe, just maybe, such thing may be useful in some other language where refactoring is problematic (which ones? all popular languages I know are either dynamic and don't need such tricks or have decent IDEs/compilers which make refactoring easy), but in Rust it's much better to just implement trait directly on type and later, if needed, introduce trait.

If you are designing API for some core library you may need to think a bit differently because refactoring wouldn't be so easy, but I think it's bad idea to push such ideas in book for the beginners: trying to teach too many things simultaneously means, 9 times out of 10, that reader would just mix everything in his (or her) head and would learn nothing.

4 Likes

Messier's book is not too bad, but it has some problems. I have already studied The Book and I wanted something intermediate before Blandy & C, which is quite in-depth. Messier has this idea of teaching by example instead of "starting from hello world". The idea of seeing some practical applications is nice, in theory, but ultimately it does not work too much in my opinion: it is difficult to follow and it is unclear which kind of reader is the intended target of the book. Also, some stylistic choices are questionable (such as the one this thread is about).

I have now started Youens-Clark's Command Line Rust and so far I like it.

1 Like