Are traits useful for the simple hobbyist?

I think I do understand more or less what a trait is and how to write one. I also think I understand how traits 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 traits is rather complicating things. I guess the fact that I come from “simple” languages like C, Pascal, BASIC, assembly, doesn’t really help me get a feel for what I am missing.

So I am not asking to convince me, I am not trying to argue. But I do notice that traits are an important concept in Rust, going with what I read and hear, but I have trouble to see the significance for a single programmer on a single, not too complicated piece of code.

Could someone give me a general idea to work with? My objective is to learn!

1 Like

Without traits, you basically don't get to write any sort of generic code. If you don't have trait bounds, then you can't usefully do anything with a value of an unknown type, beside moving it around in memory (and related low-level operations like querying its size).

Have you ever written a println!() call? Then you used the core::fmt::Display trait.

1 Like

I do understand I need to use traits. But I am trying to understand why I would need to write traits, why are they useful to use in your own code, besides the obvious use in code that needs to be re-used?

Maybe my question is not clear, because I lack the understanding to ask the correct question.

1 Like

I really don't agree at all that traits are a "professional" feature.

Traits are just a way of describing a property or interface that types can share. (Note: I'm using those words in a general, colloquial sense, not in a specific "this is the name of a language feature" sense.)

Like, you don't need structs when you can just use multiple variables. But the code is a lot clearer when you have a construct to communicate the idea behind what you're writing.

... a long, long time ago [1], I didn't see the point of first-class functions. Then I saw a single dozen-line example of Python using map and filter to process a collection of data. It's the closest thing to a religious revelation I've had in my life. I never, ever thought about programming the same way again.

Maybe you just haven't had that moment with traits, and it's true you don't need them to do a lot of things... but I suspect if you keep using Rust long enough you'll eventually see something that gives you that "a-ha!" moment.

Seriously, I've recently been using a language without traits, and it's so annoying. Classical class-based inheritence sucks, man. :stuck_out_tongue:

It's got nothing to do with "professionalism" and everything to do with how you think about designing your code.


  1. ... in a galaxy far away, Naboo was under an attack... ↩︎

26 Likes

One of the things you can do with a trait is to allow mock implementations.

I have a library that transforms files and then sends them over the network. But before I got the network bits in place, I wrote a local non-network implementation that stored the data in a sqlite database instead. That way I could check the correctness of the transcoder before starting work on the network parts.

Once I got the network parts done, I just switched the implementation with a single line of code change.

4 Likes

Most real-world uses fall into this "obvious" category. Good practice dictates that you should design against abstract interfaces rather than concrete types most of the time (of course, there are exceptions).

For example, if you are trying to query a database, it would be un-reasonable and impractical to write the same concrete code for retrieving every table ever. Instead, you'd define a trait that describes the column types and a method for deserializing from a row, and you'd write your database mapping code against that trait, not against MyUser and Balance and Employer three times.

A maybe non-obvious use case is using empty traits as markers, to encode domain constraints in the type system, and turn domain constraint violations into compiler errors. For example, if you are implementing a matrix type, and you want to provide an algorithm for inverting a matrix, then it obviously won't work with integers, so you'd want to restrict to floating-point numbers. In that case, you may want to add a : Real or : Float bound to your matrix element type (only implementing it for f32 and f64) in addition to what you actually need for the algorithm (maybe Add + Sub + Mul + Div), to ensure a higher degree of correctness and not end up with a matrix [[2, 0], [0, 2]] whose inverse is all 0s.

Incidentally, the standard Send and Sync traits fall into the same category – they define no associated items, but implementing them correctly (either automatically by the compiler, or manually by the programmer) is crucial for ensuring that multi-threaded APIs are actually memory-safe.

5 Likes

You can just start out without writing your own traits. The need for traits will evetually come up or you'll ask a concrete question here and traits will be the answer. Don't force yourself to use them.

6 Likes

I am not trying to force myself. But the “problem” is, I am used to the old, simple languages, like C. And anything I can do in C, I can do in Rust, I believe. But I really want to learn to take advantage of the things Rust can do and C cannot. I don’t want to write C looking like Rust, I want to learn to write Rust.

As a silly example:

for x in 0..4 {
    println!(“Hello”);
}

is correct Rust I believe, but

for _ in 0..4 {
    println!(“Hello”);
}

is nicer, I think, and afaik not possible in C. I hope I make sense here, I really want to learn those typical Rust things. Just writing something that works is nice, but writing something that utilises typical Rust is nicer I think.

But I agree, I don’t want to force myself more than my brain can take :slight_smile:

Then I suggest regularily reading this forum and codebases that you use.

One example of how you might hit a use case naturally might be the following. Let's say there is a function that you would just overload in c. You try to do it in rust but cargo tells you that doesn't work in rust. So you turn to generics (cause you really don't want to name your functions with the type of the argument in the function name) but then realize that there is no trait for the common behaviour you need. In that case you'll write your own.

1 Like

An eye-opening example may be Serde. In C, if you want to de/serialize data from/into two different formats (eg. JSON and TOML), you'll need to drive the serializer libraries manually: you'll first need to convert your data to the own "value tree" format of the JSON and the TOML library, and only then will it emit the actual serialized format. Or during parsing, both libraries will emit their own intermediate format that you'll have to re-map to your own data structure.

In Rust, if you derive the Serialize and Deserialize traits from Serde, all Serde-compatible libraries will be able to work with your data (emit from and parse into it), without you having to perform the mapping separately for every format.

Even if you decide that you need to customize the mapping and thus you need to write the impls manually instead of deriving, you still only need to write them once in total, and not once per format.

2 Likes

This is a totally reasonable attitude. Traits are a powerful tool for abstraction and it's very tempting to overuse them and write inscrutable generic code. They are like metaclasses or the MRO in Python. I think it's important to understand how they work, but when I find myself reaching for one, it's time to get away from the computer and take a walk.

4 Likes

Absolutely not. You almost never need metaclasses in Python, and if you think you do, you are likely doing something wrong.

In Rust, traits are a central part of the working programmer's toolbox.

2 Likes

After having been using Rust to create useful code for a few years now I might have said the same until very recently. I had never written a trait bound on anything, never used "where". But then...

I was writing code that connected to another machine over ssh and then issued commands to it and read back responses. But then I realised all that code that wrote commands and expected responses did not need to know or care if an ssh connection was used to do it. All it needed was something that it could call read() and write() on. So I specified that as a trait bound:

pub struct GadgetControl<T>
where
    T: Read + Write,
{
    pub shell: Shell<T>,
    ...
}

Now I can use all the methods of "GadgetControl" over a serial link to the remote machine. Or telnet. Or another kind of connection that provides a "Read" and "Write" interface.

It turns out to be useful for testing as well. I just create a dummy interface that accepts what is written an sends back expected responses. No actual link to anything required for testing/developing new features.

So I'm with other posters here. No point to go out of your way to use language features. After all simple enough programs don't need functions, just stick it all in main(). Simple code might not even need methods, or loops or whatever. But it's worth getting to know these things of course, one day you will see where you can use them to good effect.

7 Likes

When using existing traits I absolutely agree. But making new ones (which is what I thought @Abmvk was writing about) is fraught. In my experience, overuse of traits is the easiest way to write unreadable Rust.

3 Likes

I think I land right in between you two. Using traits as derives and as traitbounds: Very useful to do and to know how. Writing your own traits: Much less necessary unless you write libraries (or even just abstractions within your code that could be separated into their own lib). But it's for sure not as niche as writing your own Metaclasses in python. That's a definite red flag for 99% of code.

4 Likes

It's not like you have to make a final decision now and suffer for rest of your life, should you discover that you want to write your first own trait in couple of months... :wink:

Many tasks may not require own traits ever, some may utilize them to save tons of code, it always depends...
You know there's the part of language, you can always come back to it later when you feel you miss something.

6 Likes

I have yet to see a real-life Rust codebase where overuse of traits is a problem. People usually don't notice opportunities for abstraction, and underuse custom traits.

2 Likes

I think that “are traits useful” is a question much like the more common “how should I organize my Rust code?”: the answer depends far more on the nature of the specific problem you're trying to solve than anything else — not your experience level, not the size of the project, not the desired level of rigor or polish, and not whether you're getting paid to do it.

16 Likes

Probably. So my question is wrong. But still, have any insights?

As @kpreid said, it depends on the nature of your problem. So it is now your turn to describe more about the nature of your problem or project you're working on, so people can give you more specific advice. It would help to have a specific case (with code, ideally) where you don't know whether you should use traits or not.

1 Like