Unexpected syntax error (means deeper misunderstanding of Rust)

I can't seem to evade the following syntax error and puzzled why I'm getting it. On the referenced line 11, I tried removing the generic parameters, i.e victim = HashMap::from([, but then compiler complained that HashMap<String, String> didn't match type of victim: HashMap<String, A>. Well, duh!

use std::collections::HashMap;

#[derive(Debug)]
pub struct A (String, String);

pub static mut H: HashMap<String, A> = HashMap::new();

pub fn init_H( victim: &mut HashMap<String, A>) {
    victim = HashMap<String, A>::from([
    ("a", A("__a__", "foo")),
    ("b", A("__b__", "bar"))
    ]);
}
fn main() {

    init_H( &mut H);
    
    println!("Ini result\n{:#?}", H);
    
    
    
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error: expected one of `!`, `.`, `::`, `;`, `?`, `{`, `}`, or an operator, found `,`
  --> src/main.rs:11:28
   |
11 |     victim = HashMap<String, A>::from([
   |                            ^ expected one of 8 possible tokens

error: could not compile `playground` due to previous error

Did it? I've got another error - playground.

Oh boy! It seems you've opened a few cans of worms all at once!

Going from top to bottom:

static mut variables

These are highly discouraged in Rust -- the only instances they're used in is when writing some low-level constructs which are synchronized by OS apis. (In essence, not your day-to-day use case).

I assume you're trying to actually construct what'd otherwise be a global variable in another language. That's... not recommended in Rust, and although there are ways of doing that, it's often best to see how you can get around the global state. Keep H locally in main, and work off of that :slight_smile:, and come back to the topic of mutable globals when you're sure there are really big benefits which might come of them.

Turbofish syntax

When a type is generic and you wish to acess one of its members without elision (for example, you want to call a method on Vec<T> without letting the compiler guess T on its own), you need to use turbofish syntax.

This is written, in your case, as follows:

let x = HashMap::<String, A>::from(/* */);

Notice that the only real change is that there's a :: before the open angle bracket. Another example would be Vec::<usize>::new. This isn't always necessary though, since the compiler can often elide these things, but a good example of where it's commonly used is in .collect on iterators:

my_data
    .iter()
    .map(|x| /* */)
    .filter(|x| /* */)
    .collect::<Vec<T>>();

Assigning to variables under a reference

If you have a variable:

let x: &mut usize = /* */;

And you wish to assign to the value which x points at, then it is done as follows:

*x = 3;

This is distinguished from changing where x points to which is done with:

let mut y = 4;
x = &mut y;

Note in this case, you'd have needed to declare x itself mutable:

let mut x: &mut usize = /* */;

Strings galore

So, strings in Rust are always a bit of a pain point to beginners. We've unfortunately accrued a bit of technical debt in exchange for performance.

This is covered elsewhere and you can definitely find great resources if you look up "String vs str rust", but for now, know that "abc" is an &str, while you want String so you'd do "abc".to_string().

7 Likes

Is there more information on this topic somewhere? I'm curious what Rust strings would look like if they could be redone.

Well, I don't have resources on this on hand, but if I were tasked with redesigning strings in Rust, I don't think I'd settle on something all that different fundamentally from what we have right now.

1 Like

They can't really be redone. I don't agree with the previous post that they are "tech debt". The two different string types in Rust are not a mistake, they are the result of a very deliberate design decision, and they work well with the ownership-and-borrowing model.


By the way, what you have aren't syntax errors. They are type errors, which you can only get once your code is syntactically correct (otherwise the compiler doesn't stand a chance of parsing your code, let alone typecheck it).

2 Likes

I feel like you've missed the point of the conversation at hand.

Rust's string types are great, I don't disagree, but the reality is that software doesn't live in a vaccuum, and even if a particular design model is amazing on paper, it isn't necessarily the most intuitive thing to learn in practice.

Good language design can certainly accrue tech debt! Just because we've learnt how to use Rust's string system and have determined how well it works in practice, doesn't mean that it's entirely clear to newcomers. Same goes for all the new features any new language brings to the table (lifetimes, traits, etc.).

I'm also not of the party that says that rust's string type system is perfect either. I don't believe that any system that is designed in a relatively short (a few years vs the time that rust will be used for in the future) amount of time can foresee all of the issues of the future, and living with whatever small mistakes the language designers made at the beginning is part of using a language. I don't think it's that bad of an idea to ponder how the language could've designed things differently. It allows you to consider why things are how they are right now, and additionally, any mistakes which are found can always have a chance to be ameliorated by rfcs, prs, discussions, etc.

Thanks to the first round of very patient explanations, I've got a sample that answers my first question: the key was the missing 'turbo fish' syntax (which I had indeed seen before). So now I have this functioning sample:

use std::collections::HashMap;

#[derive(Debug)]
pub struct A<'a> (&'a str, &'a str);

pub fn init_H( victim: &mut HashMap<&str, A>) {
    *victim = HashMap::<&str, A>::from([
    ("a", A("__a__", "foo")),
    ("b", A("__b__", "bar"))
    ]);
}
fn main() {
    let mut H: HashMap<&str, A> = HashMap::new();
    init_H( &mut H);
    println!("Ini result\n{:#?}", H);
}

and it produces this output:

 Standard Output
Ini result
{
    "a": A(
        "__a__",
        "foo",
    ),
    "b": A(
        "__b__",
        "bar",
    ),
}

Modulo a bunch of warnings now about lack of snake case names ("Mandatory Defaults" -- I like the sound of that.)

As you see, I got the point of dereferencing with the *H. I also used &str throughout so I didn't have to decorate all the string literals with .to_string(). If I could do one thing to strings in Rust, I'd implement an automatic copy coercion when the originator is &str and the receiver is String. When does that fail?

But my long-term goal is for struct A to own all its data. In this example, I got away with a minimum of lifetime indicators, but they tend to grow like topsey in my n00b experience. So I'm working on one last version of this example to be the best it can be for my case.

The real value of &str comes through in functions more often than in long-living structs. Unfortunately you'll find that .to_string, .into, or whatever, are genuinely the best option in most cases.

However, in this particular case, you've landed on a bit of a crossroads.

If your strings are never modified, you can keep them as string literals -- &'static str. 'static as the lifetime of a reference (and I need to be precise about this because its full meaning needs to consider T: 'a type bounds, and such a description would be too generic to be useful for both of these uses) allows the compiler to assume that the data is never dropped -- in this case string literals are never dropped because they're imbedded into your executable. (Disclaimer! You can actually construct these safely by leaking a String in various ways, but that's unorthodox). Hence, you could use &'static str everywhere and only allow users to use string literals as keys and values (although I don't quite remember how well this interacts with .get()'s api and allowing users to use &'a str as a lookup key).

Or, you could just use Strings.

Or, if you've scaled to such a point where it's necessary, you can move onto different structures and patterns to make performance better or your life easier in the long term. An example of this I've used before is to "intern" strings kind of, by keeping a Vec<String> and a HashSet<String> and then translating strings into usizes before I ever use them in the rest of my application.

...more specifically, if they are never modified after compilation. If they're known only at runtime (even if immutable afterwards), it's still better to use String than to leak them, since leaks are easily forgotten and may later become an unexpected trouble.

To keep things simple while I'm learning, I will opt for Strings in the structs and .to_strings() on the literals. I get how I could decide case by case, but don't have to optimize that much right now.

So, can I implement an augmented String type that will automatically do the copy if the source is &str? I understand I'd have to do this to a MyString type, but I could see overriding BitXorAssign and Clone() (and where else?) and have something I could live with.

But If I can do it, why couldn't std lib (or compiler?) do it for String itself? The new behavior wouldn't break existing code... What am I missing?

Automatic copying in other languages (e.g. C++) proved to be an enormous performance footgun, so Rust chose not to implement it on purpose.

2 Likes

"It's not clear to beginners" is an old argument, but it's not a great one. If you look at questions asked by beginners about any language, you will see that they get confused by all sorts of things, obvious ones included. Therefore this is not the least bit indicative of a particular language, feature, or design decision.

You should view "good design" through the glasses of an experienced programmer. Do Rust's string types make sense if you know about ownership and borrowing? Absolutely. Do they prevent bugs, make your code faster, and provide valuable information when reading others' code? They do. Are they more complicated than needed? They aren't. Thus, I conclude that they are well-designed.

You can certainly ponder, but strings are so widespread and a basic building block that trying to "improve" them would cause nothing but endless breakage at this point. I don't know what improvements you are thinking of, but because the language got the fundamentals right, any such improvement is likely to be extremely minor, not nearly carrying its weight to be worth changing language built-ins and important library types.