Are traits useful for the simple hobbyist?

I would say that it's fine not to use traits, especially if they don't make sense to you. Try to understand what they are and how they work, and eventually they may make sense. As others have said, they're tools for abstraction, and it's hard to predict when that will present itself to you.

My guess will be that at some time, you will be writing some code (maybe that you copied/pasted from somewhere else), and you will think "hey these two sections of code are exactly the same except for this small piece of logic here in the middle", and at that point, it might occur to you to make a trait.

4 Likes

In my SDK, right now I am going to work with Entities similiar to the Entity-Component-System pattern using an Entity hierarchy, for reproducing a graphical experience using display objects and user interface controls.

I have asked here many times about efficient C++ struct inheritance in Rust around 2018, and got some feedback like:

You don't! Use composition instead

I've also been given some hardcoded solutions using generics. Another solution is trait dynamic dispatch, using delegation from common methods to a common field base.

The problem I had with the lack of inheritance in Rust was mostly just this thing of field reuse, however I think using an Entity-Component pattern looks fine for me.

Then, through traits, such as From and TryFrom, I can mix in .into() and .try_into() calls when working with my future Controls and mixing them with Entity, AgeraControl and DisplayObject. In my SDK, there will be solely two inheritance-like types that use a delegate trait that delegates to an Entity using respective Components.

#[cfg(test)]
mod tests {
    use crate::entity::*;

    #[test]
    fn test_components() {
        let entity = Entity::new();
        entity.set(10.0);
        assert_eq!(10.0, *entity.get::<f64>().unwrap());

        entity.delete::<f64>();
        assert!(entity.get::<f64>().is_none());
    }

    #[test]
    fn test_hierarchy() {
        let topmost = Entity::new();
        let child_1 = Entity::new();
        child_1.set_name(Some("child1".into()));
        topmost.add_child(&child_1);
        assert_eq!("child1".to_owned(), topmost.resolve_path(".last").unwrap().name().unwrap());
        assert_eq!(topmost.resolve_path(".last").unwrap(), child_1);
    }
}
1 Like

This question reminds me moments back when I was in school. It was mandatory for CS freshmen to take the C language 101 course. At the lecture 4 or 6, some of them start struggling and ask "I think I understand what pointer is, but should I really use it?"

To make a point, this question is quite natural. During last few weeks they managed to wrote interesting programs without pointers that solve problems like drawing pyramid on console, lookup user's age by their name etc. To defend them more, old school Fortran people lived perfectly fine with language without the pointer for decades. For competitive programming it's considered a good habit to avoid using pointers to squeeze out the last performance. You can replace pointers with indexes to the global array in most CP cases.

So, what do you think about the pointers in C? Do they useful for the simple hobbyist?

4 Likes

There's a crucial difference though: safe traits can't be accidentally used to cause UB. They can be used incorrectly, but this will be either logic error or panic, not something inherently subtle. Therefore, pointers in C can be thought of as being less useful for the simple hobbyist then traits in Rust, since they add more non-trivial cognitive load with more-or-less similar (non-)obviousness of the capabilities.

I used to write a lot of traits for trivial things. But now I use macro_rules! a lot more for simple syntax level meta-programming. Especially if I don't need to expose an api for public use.

1 Like

Well the project is working through the Rust book really. I don’t feel like I am ready for anything bigger yet. The first thing will be an implementation of Conway’s Life probably, as that is my favourite toy to start with in a new language.

I think it's the contrary. Any time you are going to create or use even the simplest data structure, you are going to need pointers in some form. This is especially pronounced in C, where pointers are the only form of indirection (even array indexing goes through an implicit array-to-pointer decay), and thus you must use them even for the simplest tasks.

We probably don't have quite the same degree of dependence on creating custom traits in Rust. C is absolutely 100% impossible to use in any parctical/sane way without reaching for pointers, even if you are a beginner writing a hobby project. In Rust, one can indeed write programs of moderate complexity (albeit pretty low quality in terms of architecture) without creating their own traits. (Not without using the standard ones, though.)

I believe the unsafety aspect is irrelevant. In C, almost everything is unsafe and incorrect usage is bound to lead to UB/memory errors/crashes/silently wrong results, whereas in safe Rust, nothing* does. That's the same deal with threads, or a great deal of other language constructs. However, this doesn't affect whether you need pointers or threads or Feature X for accomplishing a certain goal.

:slight_smile: I understand what you say. The thing is, when I learned C I already knew Z80 and x86 assembler. So all concepts in C were understandable translations of what I already knew in a more complicated way. With Rust I feel a bit different. What I very much enjoy about Rust is also what makes it harder for me to learn: a lot of it feels really new, and fresh. That makes the joy bigger, it really does, but it also makes it harder to try to imagine the use of certain ideas for me.

I am not sure, but I do think the fact that I am nowhere near a professional or semi-professional amateur, makes it a bit harder to understand the use of things I have never done or seen in my life.

I don’t want to sound very weird, but to me coding is not as much in the result as it is in the process. Someone mentioned a near religious experience while coding. I wouldn’t go that far, but I do recognise the joy of finding a solution, and the bigger joy of finding a beautiful solution, no matter the final result of the program you are making.

1 Like

I agree on the C part and on the safety part.

Maybe it is like cursing in the church as we say overhere, but “safety” is not very important to me. Not in C, not in Rust. I am used to spending a lot of time over runtime errors in C, now I am spending time over compiler errors in Rust.

What I love about C is the craziness. The ability to do crazy things, to code in crazy ways and even to destroy your work in the craziest of ways. The unsafeness adds to the thrill really.

What I love about Rust is not the safety or the great help the compiler gives. I like those, and I see why it is a good thing. But my love starts with the beauty of the code, the poetry of it.

My perspective here is that this is true about essentially any feature.

For example, I could say, when using assembly,

I think I do understand more or less what a function is and how to CALL one. I also think I understand how functions could be very useful in a professional environment, with more people working on the same code, or when writing libraries to be used by lots of people or in lots of programs.

But if I am writing simple programs as a hobbyist, for personal use, my feeling is that using functions is rather complicating things. I have trouble to see that significance for a single programmer when I could JMP to the code instead.

That doesn't mean that you wouldn't use functions, though. Nor does it mean that you'd make functions for no reason where an if would be perfectly sufficient.

Traits are one of the ways that Rust can help you write DRY (Don't repeat yourself - Wikipedia) software. You should use them when they're helpful, but you also shouldn't use them willy-nilly (see Code Smell: Concrete Abstraction). Feel free to continue to not use them until you hit the point where you're thinking "gee, it sure seems like I needed to write almost the same code a bunch of times for different types" or "gee, it's annoying having frobble_u32 and frobble_string and frobble_vec_f32 when they're all doing roughly the same thing", and only then use traits when you can see the advantage from doing so. Just keep in mind that they do exist, so you can give yourself the opportunity to consider using them, even if you decide they're not a good fit at the time.


(You can find lots of writings about this response to unfamiliar features under the name "blub paradox".)

8 Likes

There do exist libraries which overgeneralize and end up overusing traits and suffering poor errors (and sometimes pathological compiler performance) due to it. Async web framework-ish libraries (e.g. tide, warp, tower) are the most often cited examples of this. When you have where clauses an order of magnitude longer than the rest of the rest of the function, that does start to impact readability. (In fairness to these libraries, though, solid chunks of those huge where clauses tend to be compositional bounds and only look so complex due to a lack of trait aliases or other ways to compose "Trait but with its associated types further bound." You can't fake that kind of trait alias with blanket traits without some gross hacks almost worse than the where clause sprawl.) Another case where traits often get overused is concrete abstraction.

My take is that if you're writing high level script like application code — mostly just marshalling data from point A with shape B to point C with shape D — you usually won't benefit much from being generic, if the endpoints and data shapes you're working with are already appropriately and idiomatically generic. If you're writing application code which is mid-level and more reusable than the top level coordination, then you benefit from being generic, but generally the existing traits and derives from std and the libraries you're using are sufficient, and you'll rarely benefit from writing your own traits. But once you drop another level into "library" code, whether it be a "real" generally reusable library or tailored to a specific application use case, then the benefit of writing custom traits becomes invaluable.

As a generalization, the answer to "when is it useful to define and implement my own trait" is "when you want to make functionality generic over a set of types not already modeled by an existing trait." Some use cases genuinely never need to or benefit from doing so. Some use cases can't even exist without that ability.


Side note (not to anyone in particular): using traits exclusively for the ability to test with mocks is often an antipattern, and tests with mocks are often overly coupled to the implementation instead of the bigger picture / result. For glue code, using high coupling mocks to test what calls get done is desirable, but not for most other code.

If traits are used more like dependency injection, though, that's a good use of traits. As a poor example, don't mock the filesystem by saying these calls return these values, but instead provide a simple but real virtual filesystem.

Yes, it's still a "mock" backend/provider as it's not the full/complex one you use in production, but this isn't what's generally meant by mocks as a test pattern.

5 Likes

I think the more interesting version of this question is "when do I know I should think about using traits, as a programmer coming from C?"

My answer for this is that traits have two ways they can be used:

  • as generic bounds: impl Trait, <T: Trait>, or where T: Trait
  • as dynamic types: dyn Trait

Generic bounds, at least at first, are mostly the equivalent to using macros in C to stamp out a bunch of different versions of some code, but (mostly) only the ones specifically about replacing some type. That can go a lot further than you might expect, so it's used a lot more than you might do that in C but it's a good way to get an initial grip on why you might want to make your own trait for this use. (Macros in Rust also exist and can cover most of the other cases you would use C macros, but I won't go into them here as they're quite different to both generics and C macros)

Dynamic types are for where in C you would use a function pointer and a param/data pointer passed to the function (or structures of these). It's also a lot easier, so again it's something you would use more than the equivalent in C.

It turns out describing both of these uses looks pretty much the same, and the trade-offs of using generics or dynamic types are largely about performance, so Rust uses the same language feature.

5 Likes

Nobody has shown examples of what traits can do apart from helping with composability, so I will.

Traits allow making an array that contains things of different types in two very different ways:

  1. A Vec<Box<dyn Trait>> can hold anything implementing Trait. This can be more convenient than using an enum of all the things you want to put in the Vec. With the enum, adding a new kind of object is pretty tedious, whereas using the trait adding a new method is tedious but adding a new object is nice.
    They also have different performance characteristics. But you can use the enum_dispatch crate to write code with traits but compile to enums!

  2. (A, (B, (C, ()))) is a heterogeneous list made of tuples but you can't operate on it like a list without traits!

struct Here;
struct Later<T>(T);

trait Contains<X, L> {}
impl<X, T> Contains<X, Here> for (X, T) {}
impl<H, T, X, I> Contains<X, Later<I>> for (H, T) where T: Contains<X, I> {}

In the above example, trait Contains<X, _> is only implemented for lists that contain something of type X. This is an illustration of how traits are actually a programming language that is executed at compile-time.

Traits can be used to select functions, so they are not limited to just causing compilation failures. This can be very beautiful. For example, I had code that looked a bit like this recently:

  if let Some(foo) = bar {
      foo.method()
  }

That if was inconvenient due to things that I haven't replicated in this simplified version. It was also repeated multiple times. An added impl Trait allowed me to write just bar.method():

impl<T: Trait> Trait for Option<T> {
    fn method(&self) {
        if let Some(foo) = self {
            foo.method()
        }
    }
}
4 Likes

You are correct to say traits are for writing libraries, or more generally for code that is going to be re-used. An example would be std::collections::BTreeMap. Here the function that inserts an element is

pub fn insert(&mut self, key: K, value: V) -> Option
where
K: Ord

Ord is a trait that means the key must be a type that implements Ord, so it can be compared, as that is how a BTree works. If you are not writing "re-useable" library type code or interfaces, then it is probably correct to say you will not need to define new traits.

This is demonstrably not the case, as my posts above pointed out clearly (cf. markers which have nothing to do with re-usability and everything to do with semantics).

1 Like

Hmm! See Integer matrix - Wikipedia

" Invertibility of integer matrices is in general more numerically stable than that of non-integer matrices"

Well, I would say code that manipulates matrices, as per your example, is "library type code", in the same way that BTreeMap is. It could every well be put in a library ( even if a particular application didn't actually do this ).

That's not what I mean. The inverse of a matrix of all integer entries is in general not a matrix of integers. [[2, 0], [0, 2]]^-1 == [[0.5, 0], [0, 0.5]].

Here is just one example - https://github.com/geniusisme/falldice/blob/master/src/attack.rs#L204
This is my hobby project. It computes probabilities of various outcomes of dice rolls in Fallout tabletop game.
I use trait to model special effects. The are a lot of special effects in the game, and I found that employing the trait is a more convenient way to model them then an enum.

3 Likes

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.