Explain the below program in layman terms

fn main() {
    println!("Hello, world!"); 

    let elements = [ "Hydrogen" ];
    for element in elements {
      println!("-- Prints element without borrowing element var -- {}", element); // What is the element type here ?
      println!("-- Prints element by borrowing element var (provides fast read-only access to its contents) -- {}", &element);
      // if &element is "Hydrogen" in the above line, why can't compare element == &element ?
    }

    let needle = 42;
    let haystack = [ 1, 4, 6, 8, 10, 42, 34, 56 ];

    for item in haystack.iter() {
      if *item == needle { // Why do i have to use *item here ?, Why can't i do comparision like item == needle ?
        println!("Needle found in the haystack -- {}", item);
      }

      if item == &needle {  
        // Why does this work ? So by default item is always reference to haystack items ? If that is the case how am i able to print
        // "item" without using "&item"
        println!("Needle found in the haystack -- {}", item);
      }
    }
}

I'm very new to rust or for any other system programming language. Just trying to understand what this all means. Please explain these concepts in laymen terms, hoping that would easy for myself and to everyone new to understand.

Questions:

  1. println!("{}", element); - What type is var element here ?
  2. element == &element - If &element is "Hydrogen" and element is also "Hydrogen". Why this comparison won't work ?
  3. *item - Why do i have to use *item here ? Why can't i do comparison like item == needle ?
  4. item == &needle - Why does this work ? So by default item is always reference to haystack items ? If that is the case how am i able to print "item" without using "&item" ?
1 Like
  1. One way to figure out what type something is is to annotate it wrong and let the compiler tell you.
for element in elements {
    let x: bool = element;
}
 --> src/main.rs:6:23
  |
6 |         let x: bool = element;
  |                ----   ^^^^^^^ expected `bool`, found `&str`
  |                |
  |                expected due to this

So here the type is &str, also called a string slice. You can also tell this from the fact that elements is a collection of string literals (in this case, just one string literal "Hydrogen") which are &strs, so iterating over it in a for loop will have each item be of type &str. You can find more information in the book.

  1. element has the type &str, so &element has the type &&str. Though they both ultimately point to a string that contains the text "Hydrogen", neither one actually is "Hydrogen" (this is explained further in the link above). You can only compare two types if there is an implementation of the trait PartialEq. For example, you can compare a String with another String because there is an implementation for it. There's no implementation of it that would allow comparison of &str with &&str, so it doesn't work. Put simply, there's no code in the standard library to explain how the two should be compared, even if it's intuitively obvious.

  2. Same answer as for 2., needle's type is i32 (the default for integer literals like 42) and item's type is &i32. The reason item is a reference is that you wrote haystack.iter() in the for loop, and .iter() returns an iterator over references. If you don't want that, just remove the .iter(), and item's type will match needle.

  3. For the first question, same answer as for 3, the types are different and adding & in front of needle makes the types the same, &i32. For the second, you can print anything that implements Display. Display is automatically implemented for &item if item implements it, thanks to this implementation, and item implements it because there is an implementation for i32.

Let me know if anything is unclear and I (or someone else) will try to expand on it more.

2 Likes

I'm also (relatively) new to Rust, but I'll attempt to answer each question anyway. If someone sees I'm making a mistake, please correct me.

First let's look at the type of elements. It has type [&'static str; 1], that means it is an array of length 1. Each element of the array is of type &'static str, i.e. it is a reference to a statically allocated string (str).

When you write for x in v, then v will be turned into an iterator, and and each value returned by the iterator will be available in x. Because the elements in the array are a reference, they can be copied, thus element will be a copy of each element of the array, i.e. x has type &'static str.

Side node: I like @Heliozoa's trick with the type annotations (and I realized I just used it myself), but I think care should be taken when using that trick, because some types can be coerced. For example

let element: &'static str = &&&&&&&element;

will not throw an error.

  • element has the type &'static str
  • &element has the type &&'static str

Note that when invoking println!, it's possible to use either an owned value or a reference to it (or a reference to a reference to it) because println! is a macro. It will borrow as necessary to not accidentally "consume" your value, i.e. if you change the code as follows:

fn main() {
    println!("Hello, world!"); 

-   let elements = [ "Hydrogen" ];
+   let elements = [ "Hydrogen".to_string() ];
    for element in elements {
      println!("-- Prints element without borrowing element var -- {}", element); // What is the element type here ?
      println!("-- Prints element by borrowing element var (provides fast read-only access to its contents) -- {}", &element);
      // if &element is "Hydrogen" in the above line, why can't compare element == &element ?
    }

Then the program will still work (Playground).

But if you additionally do not use the println!() macro but a custom function (that takes a String, for example), then the code would not compile anymore (Playground).

You cannot compare a reference with the value. You can compare two values or two references but not a crossover. There has been a thread on this recently, as it can indeed cause confusion: Why can we not compare Values & References?

Side note: When you compare two references, you actually compare the values they refer to! (This isn't obvious, and different in C, for example.) You just can't compare a reference with a value (neither in Rust, nor should you do it in C). That you can compare two references (with the semantics that the values they refer to are compared) is due to an implementation of PartialEq on references in the standard library.

That is a consequence of not being able to compare references and values.

  • item is of type &i32
    (or of type &{integer} with {integer} being any integer type)
  • needle is of type i32
    (or of any other integer type, as the exact type can be inferred)

You cannot compare an &i32 with an i32. Using the dereference operator *, you convert the reference to an integer into the actual integer. Alternatively you could write:

    for item in haystack.iter() {
-     if *item == needle { // Why do i have to use *item here ?, Why can't i do comparision like item == needle ?
+     if item == &needle { // Why do i have to use *item here ?, Why can't i do comparision like item == needle ?
        println!("Needle found in the haystack -- {}", item);
      }

This converts the righthand side of the comparison into a reference, and as I explained above, it's possible to compare two references (which actually compares the values they refer to, so it does exactly the same).

But you either need a * on the left side, or a & on the right side.

Oh, I should have read all your questions first :sweat_smile:. Like I said above: It works because it's possible to compare two references (which will compare the values they refer to). But you can't compare a value with a reference.

item is of type &i32 here because you used the .iter() method. If you omit it, the code will not require any * or &:

fn main() {
    let needle = 42;
    let haystack = [ 1, 4, 6, 8, 10, 42, 34, 56 ];
    for item in haystack {
      if item == needle {
        println!("Needle found in the haystack -- {}", item);
      }
    }
}

(Playground)

Output:

Needle found in the haystack -- 42

Note however, that when you work with types that are not Copy (e.g. complex structs or owned Strings instead of integers or static references), you will often want to use for x in v.iter() instead of for x in v, because the latter would could actually take out each value from v and thus "consume" v, i.e. v is gone afterwards (you can't iterate again over v).

When you use println!, it is a macro invocation (you can see that because of the exclamation mark). The println! will automatically take a reference for you. If you have a custom print function which expects an i32, then it would not work to pass an &i32 to it (Playground).

I hope that helps understanding things better (and hope I didn't make any mistake in my explanation).


P.S.: I also found the distinction between

  • for x in v
  • for x in v.iter()

quite confusing when I was beginning with Rust. Same as it can be confusing to have to strictly distinguish between values and references. With some practice it becomes quite easy, but when I was new to Rust, this constantly confused me too (and still sometimes does, when I have references of references suddenly).

Feel free to ask any more questions if my explanation wasn't clear in some point.

2 Likes

I forgot to mention that even if both are printed as "Hydrogen" (by println!), they are different things:

  • &element is of type &&'static str, i.e. a reference to a reference to a statically allocated string,
  • element is of type &'static str, i.e. a reference to a statically allocated string.
1 Like

Thank you so much for explanation. I'm trying to digest all. Will let u know if i have any.

Thank you so much for explanation. It is very confusing language to me. Because i only know Javascript. This is so new too me.

In JavaScript, there is a distinction between primitive types (e.g. integers) and objects. But even there, you have something like references. Actually when you "store" an object in a variable in JavaScript, you don't store the object, but a reference to it.

Thus in JavaScript:

  • working with objects means you actually store a reference,
  • working with primitive types means you actually store the value.

In Rust this isn't as rigid. You can have references to primitive types (such as a reference to an integer), or you can store or pass ("owned") complex values such as structs/objects.


I don't want to overload you with information, so just ignore the following if it is confusing, but maybe it helps to better understand.

To give an example why JavaScript indeed has references (even though you don't see them):

// JavaScript code
let t = {};
t.a = 1;
let u = t;
u.a = 2;
console.log(t.a); // prints 2 (instead of 1)

(Perhaps that's known anyway, but it sometimes can confuse even experienced programmers, and I often stumble upon this when teaching Python to people who start programming.)

In this code, we'd normally say "t is an object". But what's actually stored in t is a reference to the object. When we write let u = t, then we copy the reference and now both t and u are references to the same object. Thus if we set u.a to 2 and later read t.a, we will get 2.

This can be a source for programming errors (because t.a might unexpectedly change).

If we attempt to write the same code in Rust, this won't work:

#[derive(Debug)]
struct S {
    a: i32,
}

fn main() {
    let mut s = S { a: 0 };
    let t = &mut s;
    t.a = 1;
    let u = t;
    u.a = 2;
    println!("{:?}", t); // we can't use `t` anymore!
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `t`
  --> src/main.rs:12:22
   |
8  |     let t = &mut s;
   |         - move occurs because `t` has type `&mut S`, which does not implement the `Copy` trait
9  |     t.a = 1;
10 |     let u = t;
   |             - value moved here
11 |     u.a = 2;
12 |     println!("{:?}", t); // we can't use `t` anymore!
   |                      ^ value borrowed here after move

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

This is because Rust does not allow us to have multiple mutable references to the same value/struct/object, unlike JavaScript where there can be any number of references (which always allow write-access in JavaScript) to an object.

Bottom line is:

We have references both in JavaScript and in Rust. But in JavaScript they are more hidden. Rust, in contrast, lets us decide whether we want to work with

  • owned values (e.g. value),
  • shared references (e.g. &value),
  • mutable references (e.g. &mut value).

The & operator converts a value to a reference (to that value). The * operator does the opposite.

3 Likes

I went through this page to understand slices The Slice Type - The Rust Programming Language
As per the tutorial, string slices will make sure we keep index in sync when the string provided clears

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=45cb2d84ad4e3163c9360f1f6b043ee8

But as per the code in that playground in the both String case and string slices case the variable word has "5" in one output and another has "world" even after string is cleared.

So slices make sure even if the original word is cleared, we keep the copy of that word ?

Let me go through both cases separately:

let mut s = String::from("Hello World");
let word = first_word(&s);
println!("string {}, Word {}", s, word);
s.clear();
println!("string {}, Word {}", s, word);

Note that first_word in your example is a function that returns an integer. The integer will be calculated only once. This is similar to the following example:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    let mut a = 2;
    let mut b = 3;
    let c = add(a, b); // we only calculate the sum once here
    println!("a = {}", a);
    println!("b = {}", b);
    println!("c = {}", c);
    a = 500;
    b = 12000;
    println!("a = {}", a);
    println!("b = {}", b);
    println!("c = {}", c);
}

(Playground)

Output:

a = 2
b = 3
c = 5
a = 500
b = 12000
c = 5

Now to the second part of your example:

let mut s2 = "Hello World";
let word = first_word_using_slice(&s2);
println!("string s2 {}, Word {}", s2, word);
s2 = "";
println!("string s2 {}, Word {}", s2, word);

The tricky part here are the types. When I add type annotations, it looks like this:

fn main() {
  let mut s2: &'static str = "Hello World";
  let word: &'static str = first_word_using_slice(&s2);
  println!("string s2 {}, Word {}", s2, word);
  s2 = "";
  println!("string s2 {}, Word {}", s2, word);
}

(Playground)

We start with s2 being a reference to a statically(!) allocated string. I.e. that string "Hello World" will be existent for the whole runtime of our program. More precisely, s2 is a slice, i.e. it doesn't only refer to the beginning of the string but also contains the length.

The return type of first_word_using_slice is &str, i.e. we also return a slice (which is basically a pointer to the memory location of the start of the string + length information). We store the return value in word.

Now both s2 and word are references (more precisely: slices) to statically allocated memory. s2 points to "Hello World", and word points only to the first part: "Hello".

Now when we write

s2 = "";

then we're only changing the reference/slice s2, but we do not change the statically allocated memory that we pointed to (i.e. "Hello World" is still in memory). Instead s2 now points to a different memory region (an empty region of length 0). The variable word remains unchanged and still points to the first part of "Hello World", i.e. "Hello".

1 Like

Thanks Jbe.. But Can you explain this part

  1. i.e. "Hello World" is still in memory ?

As far as i see the value of string slice changes each time it reassigned - Rust Playground

But my understanding from your answer is
All 3 assignment values are still in memory, is there way i can access "Hello World" after s2 is assigned as "ABCD" ?

let mut s2: &'static str = "Hello World";
s2 = "";
s2 = "ABCD";

Yes, correct. The slice will be reassigned to point to different memory locations each time you assign something to s2.

That is also correct (i.e. the three strings are still in memory, not the references/slices to those strings).

Addendum: AFAIK, the three strings are already in memory before the assignment too. They are in memory during the whole runtime of our program: before and after the assignment.

No, there is no way. At least there is no way in safe Rust, because even if it is still in memory, you don't know where it is in memory, and even if you do, the Rust compiler won't let you access that memory location…

…unless you guess the memory address (or memorize it somehow) and tell Rust to ignore safety precautions by using the unsafe keyword (which is something you should avoid, especially when using Rust as a beginner). But let me do it anyway to show you that the string "Hello World" is still in memory:

fn main() {
    let mut s2: &'static str = "Hello World";
    
    // let's keep a backup copy of Hello World's memory address and length!
    let memory_address = s2.as_ptr() as usize;
    let length = s2.len();
    
    println!("Our Hello World string is at address {}", memory_address);
    println!("What is s2 {}", s2);
    s2 = "";
    println!("What is s2 {}", s2);
    s2 = "ABCD";
    println!("What is s2 {}", s2);
    
    // let's try to access the memory at the stored address
    println!("We still know the memory address: {}", memory_address);
    println!("And we still know the length: {}", length);
    // DANGEROUS CODE:
    unsafe {
        // Bad things will happen if you uncomment the following line:
        // let length = length + 50;
        let recovered_bytes: &'static [u8] = std::slice::from_raw_parts(memory_address as _, length);
        let recovered_str = std::str::from_utf8(recovered_bytes).unwrap();
        println!("It's still here: {}", recovered_str);
    }
}

(Playground)

Output:

Our Hello World string is at address 94356192780418
What is s2 Hello World
What is s2 
What is s2 ABCD
We still know the memory address: 94356192780418
And we still know the length: 11
It's still here: Hello World


P.S.: We could also just re-assign s2 to "Hello World" and then the compiler might or might not choose to point to the same memory area to represent the string. You can see the experiment here: (Playground) But as far as I know, it's not guaranteed that two string literarals "Hello World" and "Hello World" will actually always point to the same memory address. You cannot rely on that.

1 Like

To explain it in simpler terms:

When you use a string literal like "Hello World", then you tell the Rust compiler to ensure that at some point in memory, there will be the characters 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' (during the entire runtime of your program). And when you do the assignment, you create a slice that references this memory location.


Some further information (if you like to read):

What you may also see in Rust is a growable string of type String. Those are dynamically allocated and their memory can be releases/freed.

Example:

fn main() {
    let static_string: &'static str = "ABC";
    let mut growable_string: String = "ABC".to_string();
    println!("static_string = {}, growable_string = {}", static_string, growable_string);
    println!("Let's grow:");
    //static_string.push_str("DEF"); // doesn't work with static strings
    growable_string.push_str("DEF");
    println!("static_string = {}, growable_string = {}", static_string, growable_string);
}

(Playground)

Output:

static_string = ABC, growable_string = ABC
Let's grow:
static_string = ABC, growable_string = ABCDEF

Note that when you work with static strings (str), you usually use references/slices to them (&str), while a growable string is of type String and you often work on those types directly (without &).

I hope that wasn't too confusing. Feel free to ask more if something is unclear.

1 Like

Amazing Jbe.. i really want to know Rust as much as you do. You have very in depth knowledge.. Thank you for patiently explaining the concepts with examples.

1 Like

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.