[ANN] Practical Rust

Hello folks, long time no see :slight_smile:

I've been writing Practical Rust, a blog where I've been exploring different aspects of Rust programming. Here's some of the content I've published so far:

Just thought that some of you may enjoy this :pray: – I've got more in the pipeline, as you can read here.

Hope you like it and feel free to spread the word elsewhere :speaking_head:


/ Leandro


A couple of comments:

  • to the HashSet article:
    • what you call "fixed-size slices" are not slices; they are called "arrays" in Rust. Slices are the dynamically-sized counterpart to arrays, spelled [T].

    • What I mean is that you can't always immediately tell what thing you can use the from function on – can you do from(vec)? can you do from(iter)? Only one way to know: ask the compiler.

      This is certainly a useful and practical way of finding out, but it would be important to mention that rustdoc-generated documentation automatically includes trait impls, so it's not guesswork – From is usable when the documentation says so.

    • The implementation doesn't need the empty case. You can just use $()* instead of $()+:

      macro_rules! set {
          ($($x:expr),* $(,)?) => (
    • The macro would be more robust and useful in real code if you spelled out the fully-qualified path, i.e. std::collections::HashSet. Otherwise users of the macro may be left wondering why their code doesn't compile and why the compiler complains about the "unknown" HashSet type.

    • We didn't include it because HashSet::from_iter(x.into_iter()) tends to require type annotations

      FromIterator::from_iter() only takes a value that implements IntoIterator; Iterator is not required. So you don't need to explicitly convert at the call site; HashSet::from_iter(the_into_iter) should work as-is. Also note that the idiomatic way would be to .collect() instead.

  • to the router article:
    • Even though it's about types, the text routinely skips over the question of ownership, and often contains example code that has trivial syntax or semantic errors.
      • For example, putting an unsized trait object inside a Handler is in itself problematic (because it will require making the field a generic parameter to make use of unsizing coercions to be usefully instantiable), but it straigh up doesn't work if you want to put the now transitively unsized Route in a Vec.
      • Another example is how the HandlerResult::Ignored variant doesn't give the Request back, even though everything seems to pass the request around by-value (and it's hard to imagine that Request is Copy).

Thanks for the notes @H2CO3! :pray:

You're right! These are arrays :slight_smile: – however, in a day-to-day situation the words array, slice, and even vector, can all be used with less precision because of context, and "array" specifically is associated in many languages with things that can grow, so not fixed-size. Writing "fixed-size slice" seemed like a good way to communicate my model readers (someone who's interested in learning and using rust) exactly what I meant without being too pedantic on the lingo.

I think this is subjective – it may be a matter of how I have set up my editing experience, but I find it easier to jump to the code of the library and search for the impl blocks there. I rarely check the docs for this.

You are right tho, that it would be in the docs, assuming you're browsing the right version of the docs.

I found it easier to explain when it has one.

Good point! I'll amend this in case someone ends up copy-pasting it.

You're not wrong but this doesn't work the way you expect it to. With our without .into_iter() you get the same experience.

Suppose I have the macro as this, so it will support converting from an impl IntoIterator and a slice:

macro_rules! set {
    ($x:expr) => {
    ($($x:expr),* $(,)?) => {

When calling it with a slice it works:

let x = set!{}; // this is inferred to be HashSet<{unknown}>
let v = vec![1,2,3]; // this is inferred to be Vec<i32>
let y = set!{v}; // this can't be inferred on stable

And you'll see an error like:

error[E0282]: type annotations needed for `HashSet<i32, _>`
  --> core/src/lib.rs:15:9
15 |     let set = set!{x};
   |         ^^^
help: consider giving `set` an explicit type, where the placeholders `_` are specified
15 |     let set: HashSet<i32, _> = set!{x};
   |            +++++++++++++++++

For more information about this error, try `rustc --explain E0282`.

In practice, I found it less useful because I had to type in the name of the set I was using, which was one of the things I was trying to avoid.

Using x.into_iter().collect() also doesn't work because there may not always bee enough information to infer the type you're collecting to, so you end up having to annotate: $x.into_iter().collect::<std::collections::HashSet<_>>().

Trade-offs, really.

Alas, in my testing and copying and iterating I may have had some small errors. I'm sure they didn't prevent you from understanding what I was trying to communicate, but in case they did, I'll do another round and correct them :ok_hand:

I've found that Ownership is rarely _needed to model and understand domains, and usually pops up during implementation. Unless you want to make it a part of it. I'd be happy to consider real-life use cases you've had where Ownership was fundamental to understanding a domain problem.

Happy to hear how you'd model this too.

Again thanks for taking the time to read and leave notes! Super appreciated :pray: I'll be amending things and thanking you in the footer.

In case you want to have some better terminology for the phenomenon of from_iter vs. from:

HashSet::from is deliberately only implemented for the case where the HashSet’s hasher is the default one. The type HashSet has a second type parameter S: BuildHasher and there is a default implementation of BuildHasher called “RandomState” that is being used if you write a type like HashSet<Foo> without specifying the second argument. But due to the way these default arguments work, just writing HashSet::from_iter([a, b, c, …]) still leaves the hasher unspecified, whereas from deliberately is kept less generic in order to be more convenient to use.

For a macro definition that would involve from_iter, to avoid the error you’ve shown, something you could do is to write HashSet::<_>::from_iter(…) instead of HashSet::from_iter(…). The type HashSet::<_> still leaves the item type inferred but specifies the hasher to be the default one.

1 Like

Hm, exactly in what way do you think I expected it to work?

And that's precisely why I suggested leaving it off. If it's not needed, why write additional code that doesn't change the meaning?


I used the "you" as in "one would" :slight_smile:

Good point! Sometimes more code is easier to maintain, or read, or understand, than less code. In this case there isn't much to gain, but also the cost isn't crazy high either.

Excellent point @steffahn! Yes, come to think of it, having a HashSet::default() in the empty branch means you can pick the hasher, and that will break if you use the macro in places with and without arguments. Or as you suggest, force both to use the same hasher. Thanks!

Good point, I didn’t spot that. But indeed HashSet::new vs HashSet::default do a similar differentiation with the former deliberately only supporting RandomState to help with inference.


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.