let thread : &'static Rust = "The Rust Thread".into()

KallDrexx

Ars Tribunus Militum
2,040
I don't know if this applies, but I read somewhere recently (perhaps /r/rust) something along the lines that Tokio allows you to use a single-threaded executor and that effectively relaxes some code requirements.

I have seen the single threaded tokio runtime, however I don't totally see how that helps. From what I can tell Tokio runtimes are meant to be swapped in and out, meaning that from a compiler perspective it has to assume that each task may be run in it's own thread. Since rust only lets one function have a mutable instance of a value at one time you can't share that mutable instance across all your different tasks (even if they are all inlined via closures the compiler still can't prove who will have mutable access at any given time).

So sure I could use RefCell<T> but there's no way to prove that all borrow calls are scoped correctly all over the place so that a mutable borrow doesn't panic your whole system down. Even with a single threaded runtime I can see this causing surprises.

I did get up and running with Mio for a base but I feel like I should tackle my confusion head on and just try to figure out how to make Tokio or async-std work. So far the conclusion I've come to is I need to treat each high level async function as an actor and use the actor model with channels for message passing between them. This is the only logical conclusion I can see to make this work in a somewhat manageable fashion.
 

koala

Ars Tribunus Angusticlavius
7,579
So I've spent some hours this weekend trying to advance with my experiments using Rust on my hand held console, so I'm trying to do some Asteroids-like game with vector graphics.

It feels nice to do this stuff again. It's been ~20 years since I last pushed pixels on a screen, and it's fun and rewarding.

However, it's also hard. Battling the borrow checker is one (today was my first time I had to use lifetimes, I think. And getting to std::boxed::Box<&'a mut [u8]> was not easy), but what runs fine in my laptop is too slow on the console, even with --release.

SDL 1 (what the device supports) is quite raw, so:

  • I had to implement a line-drawing routine. I wasn't in the mood for relearning Bresenham, so I did the implementation I can do from memory- a naive, but not terrible interpolation approach. It does lots of FP, which sounds like the MIPS processor doesn't handle well
  • I have no idea what's a performant way to use the framebuffer. Rust-SDL's screen struct has a method with_lock that takes a closure, then calls it with a raw array of bytes you can modify. I've made all my drawing code into a single closure, but I'm converting RGB to two-bpp and doing the array calculations myself, plus I need to box the array and I suspect I'm not doing that in a performant way.

It's quite entertaining, and I've been able to wrap most complexity so the main game Rust is quite simple, though, so if there was a good, high-level SDL1 (or Linux framebuffer-friendly) friendly graphics library, this would actually not be terrible at all (except for the pain in getting Rust to compile for this device, which means buildroot and ugly hacks).
 

koala

Ars Tribunus Angusticlavius
7,579
Doh, I'm dumb. I was checking everywhere to see why my simple code was so slow... but it wasn't.

I put in an FPS calculation and I'm getting 60fps on the RG300. The problem is that on my laptop I get 300fps (I expected that to be capped, for some reason).

So I just need to adjust my code to handle variable FPS, or cap it, or something. I haven't done games since the Amiga, so I'm not sure what's a good approach.

In any case, this probably means that Rust + SDL 1 on the RG300 has power to spare, so I shouldn't have issues getting my game to perform decently.
 

koala

Ars Tribunus Angusticlavius
7,579
I’ve never had boxes to a mut ref. What’s the use case?

Hmmm, might need to review that. If I remember correctly, the compiler forced me to box [u8] because that's variable size and it didn't like it. However a ref shouldn't be variable size.

TBH, I bashed at it until the compiler accepted it.

In any case, I found out that my slowness was that the stack on my device caps stuff to run at 60fps. I was getting 300fps in my computer so the device seemed broken slow. But it's not. Now I just need to find a reliable way to cap to 60fps which works both for my laptop and for the device.

Good news: the RG300 is beefy enough and if you are willing to build buildroot with some patches I've collected, Rust + SDL works well enough for simple vector graphics. Blitting should be good too.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
Just started learning rust with some toy problems. One thing I couldn't figure out was how non-trivial datastructures work with the ownership model since that implies long lived references. I set out to implement a linked list. It "works" but I'm not sure if this is idiomatic rust.

Code:
struct Stack<T> {
	_head: Option<Box<StackNode<T>>>,
}

struct StackNode<T> {
	_value: Option<T>,
	_next: Option<Box<StackNode<T>>>,
}

impl Stack<String> {
	fn new() -> Self {
		Stack{ _head: None, }
	}

	fn push(mut self, value: String) -> Stack<String> {
		self._head = Some(Box::new(StackNode{
			_value: Some(value),
			_next: self._head,
		}));
		self
	}

	fn pop(mut self) -> (Stack<String>, String) {
		let oldhead: StackNode<String> = *self._head.expect("empty");
		self._head = oldhead._next;
		(self, oldhead._value.expect("String"))
	}
}

fn main() {
	let stack: Stack<String> = Stack::new();
	let stack = stack.push(String::from("qqq1"));
	let stack = stack.push(String::from("qqq2"));
	let stack = stack.push(String::from("qqq3"));
	let stack = stack.push(String::from("qqq4"));
	let (stack, value) = stack.pop();
	println!("{}", value);
	let (stack, value) = stack.pop();
	println!("{}", value);
	let (stack, value) = stack.pop();
	println!("{}", value);
	let stack = stack.push(String::from("qqq5"));
	let (stack, value) = stack.pop();
	println!("{}", value);
	let (stack, value) = stack.pop();
	println!("{}", value);
	println!("should panic next");
	let (_, _) = stack.pop();
}

On the one hand... it works, and ownership semantics allow compile time checks that it's safe. On the other hand, this seems awkward to someone used to C/Python/Java/etc and in those languages you'd avoid passing around the whole struct on the stack like this. On the other other hand, who cares about those other languages because they have no end of surprise runtime problems so if that can be reduced it's a pretty big win. And anyway, passing a struct on the stack in rust is probably 10,000 times less overhead than passing a reference in Python, to say nothing of its train wreck of a GC.
 

tb12939

Ars Tribunus Militum
1,797
Just started learning rust with some toy problems. One thing I couldn't figure out was how non-trivial datastructures work with the ownership model since that implies long lived references. I set out to implement a linked list. It "works" but I'm not sure if this is idiomatic rust.
You want lists in Rust - You got lists in Rust

It is pretty awkward to do certain structures in rust, like graphs, where e.g. edges reference nodes. One option is wrapping is reference counting (Rc<T>), another is storing the node identifier in the edge, and looking up the node as needed (which may fail but can be handled). You can also split the graph into 2 levels, whereby the 'edge layer' gets a reference to the 'node layer' - the borrow checker will ensure the nodes don't change while the edge layer exists. Annoying no matter how you do it, but on the other hand, implementing the same in any other language is either relying on GC, manually enforcing something equivalent to Rc<Node> on node deletion, or risking dangling pointers. And if you really can justify the pain, you can use unsafe and risk bad things.

On the one hand... it works, and ownership semantics allow compile time checks that it's safe. On the other hand, this seems awkward to someone used to C/Python/Java/etc and in those languages you'd avoid passing around the whole struct on the stack like this.
Well, you're mostly passing around a 'Stack' which is just Box, so effectively a pointer in size. Unlike regular references, a Box 'owns' the target - it's main purpose to place the target on the heap, and keep the stack part small.

However it's not really that idiomatic to consume and return self in every method - usually you'd use &self and &mut self for 'regular' methods (depending on whether they are read-only). Constructor/destructor type methods of course need to return / consume self.

Also, leading underscores are usually used for placeholders variables which aren't going to be used - e.g. function parameters, or when deconstructing tuples - to suppress the standard compiler warning.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
You want lists in Rust - You got lists in Rust
I am aware. :)

In a real project I would use the built in types, this is more of an exercise to prime my intuition for the language.


It is pretty awkward to do certain structures in rust, like graphs, where e.g. edges reference nodes. One option is wrapping is reference counting (Rc<T>), another is storing the node identifier in the edge, and looking up the node as needed (which may fail but can be handled).
Reference counting can be vulnerable to memory leaks with circular references, is that a concern for rust?

Annoying no matter how you do it, but on the other hand, implementing the same in any other language is either relying on GC, manually enforcing something equivalent to Rc<Node> on node deletion, or risking dangling pointers.
Agreed. In practice I would use the provided types without an overwhelming reason not to. I guess my learning process is to just dive into the edge cases and then when I'm not confused anymore I probably understand it reasonably well.

However it's not really that idiomatic to consume and return self in every method - usually you'd use &self and &mut self for 'regular' methods (depending on whether they are read-only). Constructor/destructor type methods of course need to return / consume self.
Ah cheers. I had difficulty getting that working with the "pop" function due to its need to both modify self as well as return a value. It needs to extract the _value field to return it which is a move, which then causes use after move problems when updating self. I'm a rust n00b so problems are to be expected. I'll play with this some more later.
 

koala

Ars Tribunus Angusticlavius
7,579
You want lists in Rust - You got lists in Rust
I am aware. :)

In a real project I would use the built in types, this is more of an exercise to prime my intuition for the language.

That's not a link to the stdlib referring to a built-in type. It's a very popular tutorial that goes in depth about how to implement linked lists in Rust and the safety challenges it entails.

I'm not going there, but it probably answers the questions you have.
 

tuffy

Ars Scholae Palatinae
863
Reference counting can be vulnerable to memory leaks with circular references, is that a concern for rust?
Reference-counted smart pointers are read-only by default, but when wrapping a RefCell, it is possible to make memory-leaking circular references if you work hard enough at it. However, the standard library does offer Weak references from smart pointers which don't have any possibility of circular references.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
Ok so I think what I was missing was mem::replace.

Code:
use std::mem;

struct Stack<T> {
	_head: Option<Box<StackNode<T>>>,
}

struct StackNode<T> {
	_value: Option<T>,
	_next: Option<Box<StackNode<T>>>,
}

impl Stack<String> {
	fn new() -> Self {
		Stack{ _head: None, }
	}

	fn push(&mut self, value: String) {
		self._head = Some(Box::new(StackNode{
			_value: Some(value),
			_next: mem::replace(&mut self._head, None),
		}));
	}

	fn pop(&mut self) -> String {
		let oldhead = mem::replace(&mut self._head, None);
		let mut oldhead = oldhead.expect("stack empty");
		self._head = mem::replace(&mut oldhead._next, None);
		oldhead._value.expect("string")
	}
}

fn main() {
	let mut stack: Stack<String> = Stack::new();
	stack.push(String::from("qqq1"));
	stack.push(String::from("qqq2"));
	stack.push(String::from("qqq3"));
	stack.push(String::from("qqq4"));
	println!("{}", stack.pop());
	println!("{}", stack.pop());
	stack.push(String::from("qqq5"));
	println!("{}", stack.pop());
	println!("{}", stack.pop());
	println!("{}", stack.pop());
	println!("should panic next");
	println!("{}", stack.pop());
}

I'm finding it quite counter-intuitive but that's what I'm doing this to learn.

I did this by trial and error until it worked so I'm not sure I understand what's happening, but I think:

-in push(), mem::replace sets self._head to None returning the node but without it being owned by the self._head reference anymore (since it's just been None'd). This same statement constructs a new node for self._head with the previous head node in the _next field.
-The None in push() could be anything. I could make another throwaway placeholder StackNode and it would work fine, with the throwaway being immediately dropped (I tried this and it worked). All we need None for is to make it clear to the ownership tracker self._head doesn't own the reference anymore.
-in pop() the situation is slightly different. in push() the problem is recursive ownership, but now the problem is unpacking the StackNode in a way the ownership tracker is happy with. the first replace() is so we can have the head node as a local. this means the previous head StackNode will be dropped once we leave this code block, which is fine and is the intended behavior.
-we do another replace() to rescue _next ownership out of the soon-to-be-dropped StackNode and put it back as the new _head.
-now the _value can be returned with ownership passing up to the caller

Is there a better pattern for this than mem::replace(<whatever>, None)? That feels awkward in a way that makes me think I'm missing something.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
One thing I like about rust is that it's a fairly small language, more like C than C++ or recent iterations of Java. Also seems like it has some very pythonesque idioms that make a number of patterns convenient. The core ownership concept takes getting used to but I think that's more due to the relative novelty of the idea due to not having much exposure in other languages.

In garbage collected languages like python or java you don't really need to learn a new concept, you can pretty much treat references like pointers with permission to get lazy about deallocation. Whereas rust's stricter rules seem more like C in terms of cognitive overhead, with the difference being that a n00b like me gets compiler errors for a couple hours when learning instead of segfaults when learning C. Much like C I assume this will get easier over time, with the long tail advantage of reduced runtime errors.
 

tb12939

Ars Tribunus Militum
1,797
Much like C I assume this will get easier over time, with the long tail advantage of reduced runtime errors.
Having been at it for about 18 months, i can say it is actually amazing how few runtime errors a compiling and even somewhat unit-tested rust application normally gives. I love writing heavily threaded code and have it just work.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
Much like C I assume this will get easier over time, with the long tail advantage of reduced runtime errors.
Having been at it for about 18 months, i can say it is actually amazing how few runtime errors a compiling and even somewhat unit-tested rust application normally gives. I love writing heavily threaded code and have it just work.
I thought I was over the worst of the learning curve and then I started on threaded code. Oh boy. It's great at catching things at compile time but there's a lot of type errors I found very counter-intuitive and not that well documented. Even things like retrieving the object in handling a SyncSender send error was highly non-obvious. The docs say "The error contains the data being sent as a payload so it can be recovered.", but how you you get the payload out of the error? Turns out it's just a tuple and you get the .0 field. Not very idiomatic when a bunch of other things use .unwrap(). And it took me ages to figure out how to encapsulate objects to send between threads. Box<...> works but Arc<...> doesn't, unless you use Arc<Mutex<...>> (but not Arc<RwLock<...>>).

I guess as I build up context on the various idioms these issues will subside but I'll still have the benefits of compile time checks.
 

tb12939

Ars Tribunus Militum
1,797
Much like C I assume this will get easier over time, with the long tail advantage of reduced runtime errors.
Having been at it for about 18 months, i can say it is actually amazing how few runtime errors a compiling and even somewhat unit-tested rust application normally gives. I love writing heavily threaded code and have it just work.
I thought I was over the worst of the learning curve and then I started on threaded code. Oh boy. It's great at catching things at compile time but there's a lot of type errors I found very counter-intuitive and not that well documented. Even things like retrieving the object in handling a SyncSender send error was highly non-obvious. The docs say "The error contains the data being sent as a payload so it can be recovered.", but how you you get the payload out of the error? Turns out it's just a tuple and you get the .0 field. Not very idiomatic when a bunch of other things use .unwrap().
A good IDE helps a lot with this - IDEA shows the inferred type of variables, so if in doubt i just let x = <thing i don't really know how to work with> and figure if its a tuple, wrapper etc.

And it took me ages to figure out how to encapsulate objects to send between threads. Box<...> works but Arc<...> doesn't, unless you use Arc<Mutex<...>> (but not Arc<RwLock<...>>).
Usually i send a read-only object via a cloned Arc, and have a Mutex or AtomicX as needed to handle the write parts. ** edit ** Seems that RwLock requires the wrapped type to be Sync, while Mutex does not, in order for the RwLock to be Sync.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
And it took me ages to figure out how to encapsulate objects to send between threads. Box<...> works but Arc<...> doesn't, unless you use Arc<Mutex<...>> (but not Arc<RwLock<...>>).
Usually i send a read-only object via a cloned Arc, and have a Mutex or AtomicX as needed to handle the write parts. ** edit ** Seems that RwLock requires the wrapped type to be Sync, while Mutex does not, in order for the RwLock to be Sync.
Yeah that is what I gathered. Some sort of multiple inheritance but not really? That's how you end up with these weird composite types.

Here's a question. I'm trying to implement a timeout for a session handled by multiple threads. The idiomatic way to do this appears to be Instant. But, Instants aren't really intended to be updated in place, so you have to replace the struct contents, which is fine, but I'm not sure how to do this in such a way as to be both thread safe and not leak memory. From what I can tell an AtomicPtr will not update the reference count in an Arc so I can't just keep making Arc<Instant>'s and swap them into the AtomicPtr as they will leak.

The workaround that I'm pretty sure will work is to make my own wrapper struct with a field I can update. I would then be able to easily protect that with a RwLock. Another method would be to just make an AtomicU64 and store unix time there, but this seems non-idiomatic and may suffer from issues in the case of leap seconds or other weird edge cases that Instant is designed to handle. I could also make a timeout handler thread and use SyncSender to update it, though that feels quite heavyweight. My ideal though would be basically a thread safe resettable Instant. Does that make sense?

Thanks for all the feedback by the way. Goes for everyone in the thread. :)

E: hm can I use std::mem::replace for this

E2: I think this will do what I want. :scared:

timeout is a Mutex<Instant>

Code:
macro_rules! touch_timeout {
	($timeout:expr) => {
		match $timeout.lock() {
			Ok(mut t) => drop(std::mem::replace(t.deref_mut(), Instant::now())),
			Err(e) => error!("couldn't update timeout value {}", e),
		};
	}
}
 

tb12939

Ars Tribunus Militum
1,797
Usually i send a read-only object via a cloned Arc, and have a Mutex or AtomicX as needed to handle the write parts. ** edit ** Seems that RwLock requires the wrapped type to be Sync, while Mutex does not, in order for the RwLock to be Sync.
Yeah that is what I gathered. Some sort of multiple inheritance but not really? That's how you end up with these weird composite types.
I think it's more that in some cases the wrapper type can add capabilities to the wrapped type, while in other cases the wrapped type 'inherits' the capability from the wrapped type - e.g. Mutex adds Sync to the wrapped thing, while an RwLock is Sync only if the wrapped type is.

Since Sync roughly means 'read-only references are sharable between threads', and RwLock potentially exposes multiple read-only references, it relies on the underlying type implementing Sync to remain safe in that scenario. On the other hand, Mutex will never actually give out multiple read-only references at the same time, so the Mutex is Sync, even if the wrapped type isn't.

Here's a question. I'm trying to implement a timeout for a session handled by multiple threads. The idiomatic way to do this appears to be Instant. But, Instants aren't really intended to be updated in place, so you have to replace the struct contents, which is fine, but I'm not sure how to do this in such a way as to be both thread safe and not leak memory. From what I can tell an AtomicPtr will not update the reference count in an Arc so I can't just keep making Arc<Instant>'s and swap them into the AtomicPtr as they will leak.
I'd definitely put AtomicPtr in the 'tools of last resort' drawer. It's probably one of the most dangerous tools in Rust, given that you have the multi-threading foot-guns coupled with the raw pointer foot-guns.

timeout is a Mutex<Instant>
Good choice. Incidentally i think an RwLock should also work in this case, since instant is Send and Sync.

From there, if justified, i can think of two possible ways to escalate:

One method would be to have a shared atomic 'change counter', a Mutex/RwLock protecting a shared Instant, and a per-thread cache of the last known change count and Instant. Assuming updates were relatively infrequent vs checks, most checks would simply be a read of the atomic change counter and comparison against the known change count.

The other would be to use a combination of a suitable atomic and mem::transmute to smuggle the Instant value in and out as required. Luckily, AtomicCell (crossbeam::atomic::AtomicCell) is basically this, and gives atomic update behaviour on anything small enough to fit inside the standard atomic types (and i'm reasonably sure Instant qualifies)
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
One method would be to have a shared atomic 'change counter', a Mutex/RwLock protecting a shared Instant, and a per-thread cache of the last known change count and Instant. Assuming updates were relatively infrequent vs checks, most checks would simply be a read of the atomic change counter and comparison against the known change count.
That's interesting. The situation is the reverse, updates are frequent (happens on any activity) and reads are infrequent. Seems like AtomicU64 would be sufficient to provide a heartbeat on any activity, and it's not actually necessary to pass an Instant between threads when you have a heartbeat. Seems like I was overthinking it. Good learning opportunity though.

Atomic add seems lighter weight than message passing or mutex protected data structures and nanosecond precision is meaningless for network service timeouts with latencies millions of times higher. Just need a heartbeat on ~secondly timeout polls.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
Hm, learning tokio the best way I've found when I want to have incompatible error types in an async function is to make an enum for all possible errors and use that in the Result<whatever, MyError> return type. This needs an impl From<whatever> for MyError block for each of the possibilities, but these are trivial. Does that seem reasonable? I thought about doing handlers like that for the upstream Error but it would be a lot harder to translate all possible errors to a pre-existing error type.

This is especially relevant when I want to have (for example) reqwest and tokio code in the same function.

My "not idiomatic" alarm isn't going off here because this seems like and intended use case for rust enums.
 

tb12939

Ars Tribunus Militum
1,797
Hm, learning tokio the best way I've found when I want to have incompatible error types in an async function is to make an enum for all possible errors and use that in the Result<whatever, MyError> return type. This needs an impl From<whatever> for MyError block for each of the possibilities, but these are trivial. Does that seem reasonable?
Haven't used tokio, but sounds reasonable in general. If any flavour of the enum gets heavy, you might want to consider a wrapper type and internal boxing, otherwise you bloat the result enum as well. Apart from that, i see no issue.
 

tuffy

Ars Scholae Palatinae
863
Packing a bunch of errors into an error enum isn't unusual, in my experience - particularly in library code where one might want different behavior depending on the error.

But if all you're planning to do is print out the error and don't care about what type it is, Result<T, Box<dyn Error>> is a more concise alternative that'll play well with anything implementing the Error trait.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
Cheers. I want to make sure I'm able to not just get it to compile but actually write idiomatic rust so the feedback is important. :)

Quite pleased with the results. My toy problem is a utility to tunnel connections through HTTP and tunneled scp downloading a large file is performing similarly to scp by itself on my gigabit cable modem. No particular effort to optimize, just trying to write rust the way rust wants to be written and "eh a megabyte is a good buffer size i reckon". Also works seamlessly for interactive ssh sessions.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
Getting lost in the type system again, wondering if there's any reasonable ways around this.

The project I wrote has a bunch of code that can either work on the OwnedReadHalf and OwnedWriteHalf from a TcpStream, or stdin/stdout. In an OO language you'd have an interface or parent class so you could pass around objects of either and it would be fine. My shitty workaround was to make an enum that could hold either of the above and then use match statements anywhere it came up in the code. My thinking was to replace the enum with a Box<dyn Async{Read,Write}Ext>, but this doesn't work:

Code:
the trait `tokio::io::util::async_read_ext::AsyncReadExt` cannot be made into an object

There's a list of functions where the return type includes "self" which apparently makes it impossible, presumably because the size of the struct is not known at compile time and the return type makes no provision for the Box<dyn> wrapper.

My next thought was to just do a giant impl block where I match on the enum and call out to the impls of the respective types for Async{Read,Write}Ext, but this seems like a gratuitous amount of work to replace a workaround that only happens in like 2 places in the whole project, which isn't worth it.

I'm now considering other options. I might do my own traits that only include the functions I want. There seems to be a crate (trait_enum) that has a macro that can generate a wrapper for a trait automatically.
 

tb12939

Ars Tribunus Militum
1,797
Getting lost in the type system again, wondering if there's any reasonable ways around this.

The project I wrote has a bunch of code that can either work on the OwnedReadHalf and OwnedWriteHalf from a TcpStream, or stdin/stdout. In an OO language you'd have an interface or parent class so you could pass around objects of either and it would be fine. My shitty workaround was to make an enum that could hold either of the above and then use match statements anywhere it came up in the code.
That's not necessarily a shitty workaround - 'put the class hierarchy into an enum' is one of the recommended options, especially if it doesn't need to be client extendable. Generics are the preferred method if you can live with static dispatch, but still want the code re-use.

My thinking was to replace the enum with a Box<dyn Async{Read,Write}Ext>, but this doesn't work
Yeah, traits generally don't end up "object safe" accidentally.

I'm now considering other options. I might do my own traits that only include the functions I want. There seems to be a crate (trait_enum) that has a macro that can generate a wrapper for a trait automatically.
Makes sense, if you need the extensibility.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
That's not necessarily a shitty workaround - 'put the class hierarchy into an enum' is one of the recommended options, especially if it doesn't need to be client extendable. Generics are the preferred method if you can live with static dispatch, but still want the code re-use.
I did try generics but I couldn't wrap my brain around how to make the lifetimes work, I always ended up with a compiler error I couldn't fix without breaking something else.
 

tb12939

Ars Tribunus Militum
1,797
That's not necessarily a shitty workaround - 'put the class hierarchy into an enum' is one of the recommended options, especially if it doesn't need to be client extendable. Generics are the preferred method if you can live with static dispatch, but still want the code re-use.
I did try generics but I couldn't wrap my brain around how to make the lifetimes work, I always ended up with a compiler error I couldn't fix without breaking something else.
Not sure i understand the connection - generics in the "generics vs trait object vs enums" sense are independent of lifetimes.

In the case of e.g. a graph and its nodes, generics with a regular struct would limit any given graph to a specific kind of node, while using trait objects would allow a mix of node types within a single graph. Enums are more like trait objects in this aspect, and allow a mix of node types per graph, and are less extensible since they get defined all at once.

Incidentally, often one of the easiest ways to overcome lifetime issues is not to use them - instead structure things to allow your more complex structs to "own" the simple ones. This works well if you don't need long-lived references from the outer struct to the sub-parts of the inner struct. You can of course arrange to give back short-lived references as needed, either to the outer struct or the client.
 

Megalodon

Ars Legatus Legionis
34,201
Subscriptor++
That's not necessarily a shitty workaround - 'put the class hierarchy into an enum' is one of the recommended options, especially if it doesn't need to be client extendable. Generics are the preferred method if you can live with static dispatch, but still want the code re-use.
I did try generics but I couldn't wrap my brain around how to make the lifetimes work, I always ended up with a compiler error I couldn't fix without breaking something else.
Not sure i understand the connection - generics in the "generics vs trait object vs enums" sense are independent of lifetimes.
I think with the specific generic with trait I was trying to do, lifetimes became important for reasons I don't entirely grasp.

What I was trying to do was a generic with a "where" enforcing AsyncReadExt, and when I did that I got a bunch of "does not live long enough" errors. I tried adding lifetimes but wasn't able to do figure out a combination that worked. Now that I mention it I think the problem may arise from the same thing that prevents it from being used with "dyn", that being the trait has methods that return self. I guess this is a case the compiler can't handle with lifetime inference and I'm not skilled enough with the language to write it explicitly so it works.

Incidentally, often one of the easiest ways to overcome lifetime issues is not to use them - instead structure things to allow your more complex structs to "own" the simple ones. This works well if you don't need long-lived references from the outer struct to the sub-parts of the inner struct. You can of course arrange to give back short-lived references as needed, either to the outer struct or the client.
Overall this is the approach I've used and been pretty happy with it, and in terms of datastructures this was an absolutely trivial example. It's simply a struct where the reader and writer for a socket or stdin/stdout are some of the fields. A tokio OwnedReaderHalf and Stdin are different types in Rust, though they both have the AsyncReadExt trait (similarly for AsyncWriteExt). That trait has methods that return Self, and I believe this is what creates the lifetime issues. Same root cause as the "dyn" problem.

I think overall I'm not really willing to put in the time to figure this out due to a number of reasons:
-Banging your head against the wall for an obscure issue is a bit much even for a quarantine boredom project. It's not fun.
-If this was production code I'd call out the enum/match/macro workaround in my code review and most likely either a) someone would know a better way that I could implement relatively easily or b) no one would have better ideas and we'd all agree it wasn't worth the time to find a better fix
-Async is still pretty new and there's a reasonable chance the issue will just go away upstream over the next few years. I found discussions on this when looking around for other ways to deal with it so I'm not the only one to hit this issue.
 

drogin

Ars Tribunus Angusticlavius
7,222
Subscriptor++
So, a bit of a thread necro here, but I figure people may actually still be interested in discussion.

I just worked through the exercises in rustlings. Generally decently well composed and organized and hinted. Does anyone have recommendations for other sources of focused, discrete exercises?

Thanks for this. I am almost done with my read through of "The Rust Programming Language". Book was pretty good, but now I need some challenges/exercises to work through.
 

Shamyl Zakariya

Ars Praefectus
3,813
Subscriptor++
So, a bit of a thread necro here, but I figure people may actually still be interested in discussion.

I just worked through the exercises in rustlings. Generally decently well composed and organized and hinted. Does anyone have recommendations for other sources of focused, discrete exercises?

Thanks for this. I am almost done with my read through of "The Rust Programming Language". Book was pretty good, but now I need some challenges/exercises to work through.

I got my feet properly wet in Rust by implementing an AST tree-walker interpreter of the Lox language from Robert Nystrom's Crafting Interpreters (https://craftinginterpreters.com/) - it was really hard! But by the time I finished I felt pretty comfortable. Here's my implementation: https://github.com/ShamylZakariya/Craft ... aster/rlox

The only thing I really don't like about my current implementation is that the tokenizer holds String copies from the source text, rather than &str refs. Someday if I'm good enough at lifetimes I'll give it another pass.

I also decided to get up-to-date on more modern GPU stuff by writing a clone of the first level of Gargoyle's Quest (gameboy game) in rust using WGPU. That was a blast! https://github.com/ShamylZakariya/Platformer
 

QuadDamaged

Ars Praefectus
3,955
Subscriptor++
Diving back into some parsing shenanigans by hand, and I really have been having a hard time not conflating the `Slice` struct and `Iter` trait at a conceptual level. I blame it on omicron.
Took me about an hour to sort-out, including writing custom iterator structs over `&'a[u8]` and to my surprise never ran in the borrow checker... And it actually works with my first unit test.

Typically I was looking for the equivalent of `std::slice::<u8>::split_at_first<P>( predicate:p)-> Option(&[u8],&[u8)` and my monkey brain couldn't understand that I either had to manually move a `Split` iterator from a `Slice` twice, or do a `slice.iter().position(p)` and use the index for `slice::split_at`.

🐵
 

QuadDamaged

Ars Praefectus
3,955
Subscriptor++
And further down the rabbit hole...

Dealing with Options... imperative vs functional style.

Option 0: Nested if... lets
Code:
    fn next(&mut self) -> Option<Self::Item> {
        let mut tags = self.buffer.splitn(2, |&b| b == SOH);
        if let Some(head) = tags.next() {
            if let Some(cons) = tags.next() {
                self.buffer = cons; // advance
                if let Some(s) = head.iter().position(|&b| b == SEP) {
                    let (tag, value) = head.split_at(s);
                    return Some(FixTagToken { tag, value });
                }
            }
        }
        None
    }

Option 1: Pattern matching
Code:
    fn next(&mut self) -> Option<Self::Item> {
        let mut tags = self.buffer.splitn(2, |&b| b == SOH);
        match (tags.next(), tags.next()) {
            (Some(head), Some(cons)) => {
                self.buffer = cons; // advance
                head.iter()
                    .position(|&b| b == SEP)
                    .map(|i| head.split_at(i + 1))
                    .map(|(tag, value)| FixTagToken { tag, value })
            }
            _ => None
        }
    }

Option 2: Functional to the max
Code:
    fn next(&mut self) -> Option<Self::Item> {
        let mut tags = self.buffer.splitn(2, |&b| b == SOH);
        tags.next().zip(tags.next()).and_then(|(head, cons)| {
            self.buffer = cons; // advance
            let mut tv = head.splitn(2, |&b| b == SEP);
            tv.next()
                .zip(tv.next())
                .map(|(tag, value)| FixTagToken { tag, value })
        })
    }


I don't find the nested ifs too bad TBH, due to the fact that I have a side-effect that becomes clearer to read with this style. And there's a slight output difference between option 1,2 and 3 (which doesn't have any separators)
Opinions welcome!