Hi all.
recently I have started working with Rust to implement some code that I had implemented with C++, then Go.
I understand the concept of security that Rust provides but it seems to me that it is so restrictive that kill productivity over time since one needs to change and restructure the code many times to the point that the natural intuitive implementation is completely gone.
I just give a simple example. I have a several layers in neural network and they communicate with each other by passing messages.
Inside main I just process two messages by the first layer and take the messages produced by the first layer and pass them to the second layer.
Rust does not allow me as apparently I am creating two mutable reference to l1 while processing first and second message.
This is just a very naive example and I do not know how one can fix it. One can definitely move the values but this may not be a good idea in data science because there are always two phases forwrad: data is processed and backward: gradient is computed.
Any idea how one can really fix this ?
// simple message containing information
struct Message{
content: f64,
channel: i64,
}
// a simple linear layer
struct Linear{
msg_in: Message,
list_msg_out: &[Message],
}
impl Linear{
fn new()->Linear{
....
}
fn process(&mut self, &msg_in: Message)->&[Message]
//do some processing and store the results in output array
return self.list_msg_out;
}
}
fn main(){
// create two layer network: just a dummy example
let mut l1 = Linear();
let mut l2 = Linear();
let msg_in = Message{....};
// process the message with the first layer
let mut list_msg_out1 = l1.process(&msg_in);
// pass the results to the second layer: here just process the first message from the list
l2.process(list_msg_out1[0])
// now process another message
let msg_in_next = Message{...}
let list_msg_out1 = l1.process(&msg_in_next)
At a guess you're using &[Message] when you should have an owning Vec<Message>, but it's hard to tell from the given example. I couldn't reproduce a borrow error, but I also had to make a lot of adjustments before the code compiled.
I apologize for not publishing the whole code which is a little bit complicated.
yes internally I represent the list_msg_out as a vector and pass a slice of it as &[Message] to the next layer. The problem is, as I mentioned, when I do this, I cannot use the linear object l1 more than once as Rust complains that I am using a mutable reference twice, which is quite annoying.
Please see the following example code, which is not exactly what I have but quite similar:
I give another example: suppose we have an struct that contains a reference. Try to write a function new that returns an instant of that struct, good luck It is almost impossible.
How to initialize this reference:
create a Message object inside new1() and use it for temporary initialization. This does not work since that object gets destroyed at the end of new() and this reference becomes dangling.
Pass a dummy message to new2() so that this reference is initialized temporarily. This also does not work since that object gets sinked inside new2 and the returned reference gets dangling.
Return the passed dummy object together with the desired object. This also does not work since if the dummy is not used later after returning from function, that also gets destroyed.
I am not a novice programmer but things like this almost kills the development because normal simple things are impossible in Rust.
That does indeed fix the example given (playground).
The reason why lifetimes on &mut self is almost always not what you want is that lifetime annotations are supplied to tie together different lifetimes in a function signature. If different lifetimes are wanted for every input reference, and there is either only one input reference or a &self or &mut self reference that should have the same lifetime as an output reference, then the lifetime elision rules take care of those cases and explicit lifetimes need not be written.
In fn forward(&'a mut self, msg_ref: &'a mut Message), the lifetime of the &mut self reference is tied to the lifetime of the msg_ref that the processor contains, meaning the type of the first parameter to forward() is of type &'a mut Processor<'a>. With shared references (&) Rust can be quite forgiving of this odd pattern since it can pop in any compatible lifetime, but unique references (&mut) are invariant, which means the lifetimes they are instantiated with have to match exactly (the nomicon has a lengthy explanation as to why, in case you're interested). Because &mut is invariant, it means the lifetime for &'a mut self must be the same as the lifetime of the contained messages, which are used later in main(), so there is no option but to borrow self for the whole of main(). Without the lifetime annotaion on &mut self, it gets its own separate lifetime, which only borrows for the call to forward() then allows other things to borrow self.
Thanks a lot for the reply.
After your edits, the program runs but when I just repeat the same procedure by asking to process the same message again, it does not compile.
I would be grateful if you take a look at this link:
References in Rust are typically for short-lived borrows, and not long living data structures. And a core tenant of Rust is that memory reachable by a &mut cannot be aliased -- once something that moves, changes, or even gives the ability to examine msg is created independent of an existing &mut msg, the &mut msg can no longer be valid.
So the example can never work, as e.g. the second call to process.forward(&mut msg) results in two &mut to the same object being alive within forward. [1]
Thank you so much for restructuring the code.
I think I need to practice more to get used to this way of writing code with Rust, which seems to be a little bit different than other languages.