Rust lists vs Kotlin lists

I've had a question about Rust from a friend. They are proficient in kotlin, but they don't understand the mutability concepts of rust. What they asked me is this:

"How would you do the following in Rust?"

import kotlin.test.*

fun main() {
    val a = listOf(1, 2, 3, 4, 5)
    val b = a.filter { it < 3 }
    a[0] = 10
    assertEquals(
        b[0],
        10,
        "Modifying a results in modifying b, because the elements are references"
    )
}

I've programmed in rust for over a year now, and I can't seem to figure it out. The first thing that looked odd to me is that both a and b are declared immutable by the val keyword and thus they cannot be reassigned, but they can be mutated. Anyway, back to my question. How would you do this in Rust? Preferably without using unsafe. I'd imagine how one could use raw pointers to allow modification of list a after referencing it.

Since this would be modifying the initial list from under the iterator (the iterator would be borrowing the list, so there's an immutable borrow), you couldn't do that.

1 Like

You can do it safely with shared mutability, aka interior mutability.

But a more honest answer is "you don't really do things (exactly) like that in Rust".

13 Likes

that's not simple in rust, you can't mutate a value while there is a reference to it.
the following code, here's it in the playground

fn main() {
    let mut a = vec![1, 2, 3, 4, 5, 6];
    let b = a.iter().filter(|&&x| x < 3).collect::<Vec<_>>();
    a[0] = 10;
    println!("{}", b[0]);
}

doesn't compile because we can't mutate a since it is borrowed by b.

The following code, however, works, link to the playground

use std::cell::RefCell;

fn main() {
    let a = (1..=6).map(RefCell::new).collect::<Vec<_>>();
    let b = a.iter().filter(|x| *x.borrow() < 3).collect::<Vec<_>>();
    *a[0].borrow_mut() = 10;
    println!("{}", b[0].borrow());
}

By using refcell, we tell the compiler that we will take care of the ownership rules ourselves, but it adds an extra layer of complexity.

But, as said before in the thread, that isn't really how you write code in rust... to actually give you a better solution we'd need to know what are you trying to achieve with that code

1 Like

I would do it like this:

fn main() {
    let mut a = [1, 2, 3, 4, 5];
    let b_indices: Vec<usize> = (0..a.len()).filter(|i| a[*i] < 3).collect();
    a[0] = 10;
    assert_eq!(a[b_indices[0]], 10);
}

a owns the data, b_indices just refers to what a owns.

The Cell approach that quinedot shows is somewhat enticing to me too though.

2 Likes

Yeah, the list/vec sort of hides the problem, which is trying assign to something that has an active reference to it already.

For example, this doesn't compile either:

fn main() {
    let mut a = 1;
    
    let b = &a;
    
    a=10;
    
    println!("{:?}", b);
    
}

So for me, the answer is either "interior mutabity", or "rethink how you've modeled the problem"

Yup, using b_indices can be a hassle when you need to do it multiple times.

I get that, references are either a single &mut or nonzero &'s, never both. I've never really looked into interior mutability, but quinedot showed how you would do that and if I ever really need to do this in rust, I would probably approach it that way.

PS: Here's a video by Jon Gjengset who want to learn about interior mutability: Crust of Rust: Smart Pointers and Interior Mutability - YouTube

Here is a slightly simpler / shorter version of what @quinedot wrote -- only one Vec rather than two:

fn main() {
    let mut a = [1, 2, 3, 4, 5];
    let a_cells = Cell::from_mut(&mut a[..]).as_slice_of_cells();
    let b: Vec<_> = a_cells.iter().filter(|c| c.get() < 3).collect();
    a_cells[0].set(10);
    assert_eq!(b[0].get(), 10);
}

3 Likes

Which is precisely the point. Interior mutability is a hassle, too.

So, basically, the question becomes a bit like “here's the nice collection of footguns: one can be used to shoot yourself in your foot, that one blows up your kneecaps and this one here can destroy the whole leg in one shot… where are they in the Rust?” and the answer to that is pretty natural: they are under lock and key as they should be.

Rust is designed around trying to prevent such long-distance coupling (which is everywhere in modern OOP languages). Surprisingly enough languages like Kotrlin and/or Typescript are trying to do that, too… but they couldn't do that properly because the target audience values convenience much more than safety and correctness.

Yet, unlike languages like Haskell, Rust provides some ways to get these footguns back if you really need them (although model Haskell offers that, too). But these are not easy to use and not convenient to use… but that's fine since you are not supposed to use them unless really forced by some very pressing need).

3 Likes

I don't think that kotlin snippet is valid.

First of all, yes, in Kotlin you can mutate val bindings, however their interface needs to allow that, and List doesn't, so you get an error saying "No set method providing array access". You need a MutableList for that, which you can get by using mutableListOf instead of listOf.

I would say you can emulate that in Rust, but why not just use a mutable binding in the first place? In Kotlin it works like that due to the way the language handles mutability, and a var binding doesn't make it work, but that's just how that language works.

And lastly, that assertion fails. b is a different list, and assigning an element to a will change only that list. Yes, elements are still references, so by using internal mutability you can observe changes inside elements of a in b, but integers don't have internal mutability.

2 Likes

Totally agree with you. I can't think of a scenario when you need this, they are indeed footguns anyways. I never said I liked this behaviour in the JVM, but at least now I know that it's at least possible to do with safe Rust.

The average Rust programmer doesn't need it and it's considered an advanced topic. I've never touched it and I don't think I will any time soon.

I'm sorry, I tried to change my example into something more concise, without testing. The original code snippet:

import kotlin.test.*

fun main() {
    val a = listOf(
        Person("John", 33),
        Person("Doe", 50)
    )
    val b = a.filter { it.age < 40 }
    a[0].age = 10
    assertEquals(
        b[0].age,
        10,
        "Modifying a results in modifying b, because the elements are references"
    )
}

data class Person(var name: String, var age: Int)

This only seems to happen to Object's in java, as they are handled as references in the JVM. Because of the var keyword in the Person class those fields can be modified without the need of "mutating" the array.

Ah, for this more complicated example the Cell approach doesn't work because the Person type wouldn't be Copy, which is required for Cell::get.

Using interior mutability in this case would require using RefCells which is more messy than Cell because it requires runtime rather than compile-time checks, and because it doesn't have from_mut and as_slice_of_cells.

This shows a limitation of the interior mutability approach.

1 Like

Well, it doesn't add much complexity if I understand this correctly, as jvcmarcenes did this in about the same amount of code as the Cell approach.

Note that this code snippet is the perfect way to discuss why the Kotlin approach is problematic.

Note that nothing prevents you from writing a[0].age = 45 and then ending up with the wrong set of Persons in b.

Rust approach would be to make it impossible to change a while b is alive. This ensures that b wouldn't contain incorrect information. If it contained a list of persons whose age was less than 40 when it was created then all these persons wouldn't suddenly age while we are using that data.

This may not be as convenient as what Kotlin does, but it, sure as hell, is safer… this explains why Rust have that fancy ownership concept and then lifetimes and borrow rules.

4 Likes

Yes but it's a bit risky because the compiler doesn't protect you from accidentally calling borrow_mut twice from two different places which seems innocuous but will result in a panic.

And if you want to have a regular mutable array of Person, you then won't be able to reinterpret it as an array of RefCell<Person> in some local piece of code, whereas for Cell that is fine to do.

2 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.