5 ms·
> It was a weekend learning project, and all I wanted to do was create a freshman level data structure from scratch. It's worth mentioning that the implementa
by Manishearth 10y ago
> It was a weekend learning project, and all I wanted to do was create a freshman level data structure from scratch.
It's worth mentioning that the implementation difficulty of data structures in Rust is not representative of the general difficulty of programming in Rust. Just because this is hard doesn't make it a bad language. Rust's philosophy is that you deal with the trouble of writing unsafe code each time you need a new low level abstraction, and when done, you don't have to worry about it again.
Also, writing generic datastructures in C++ without falling afoul of UB, and/or getting destruction semantics right can be quite tricky. Rust ... really doesn't have additional issues here; raw pointers in Rust work the same way as in C++, except they're a tad verbose. The only difference is that in Rust you probably want to provide a safe API, whereas in "freshman C++ datastructures" the datastructure does not have the unsafety neatly encapsulated. This is a hard task in both Rust and C++, especially with generic datastructures.
Writing datastructures isn't really a common task. It's a "simple" task because it's simple in C and C++, but here's no real reason it has to be in Rust. You can write inefficient datastructures in 100% safe Rust without too many problems, much like you can implement datastructures in regular-joe Java. Writing efficient, low-level datastructures can be pretty hard, but like I said it's a rare task so not much of an issue.
http://cglab.ca/~abeinges/blah/too-many-lists/book/ http://cglab.ca/~abeinges/blah/too-many-lists/book/ and https://doc.rust-lang.org/nomicon/ https://doc.rust-lang.org/nomicon/ are pretty good resources about writing unsafe Rust. The exact boundaries and semantics of unsafe code are still being pinned down so while I do want to improve this documentation I'm waiting for that to happen first.
In general I advise folks to not write unsafe Rust till they are sure that they have a clear grasp of safe Rust. But if you want I can have a look at what you tried and help you improve your design. I'm very interested in making such things more accessible (on one hand, the Rust community doesn't really want to encourage the use of unsafe code, on the other hand, sometimes you need it and it would be nice if it was easier for people to use when that situation arises) so learning what people stumble on is important to me.
> Unfortunately, I don't think they're very concerned about my particular use cases (and maybe they should
>
> I think all this says is that you don't write very much of the kind of code which benefits from generics. You can dismiss my point of view, but some of us do use them a lot, and it's one of the driving reasons I use C++.
It seems like you're using generics the way you use templates for metaprogramming in C++. I totally get how powerful TMP is (I love to use it myself, though I can get carried away), but be aware that Rust is a different language and you have to approach it differently. It's important to consider the limitations of the system when designing a solution -- if I designed a solution for C++ and implemented it in Rust, I'd hit problems in the last mile and find it very annoying to make it work (using macros or something to fill in the gap). However, if you design the code from the start with Rust's advantages and limitations in mind; you may come up with a different but just as good system. This is something I hit every time I learn a new language. I hit it with Rust, Go, and many years ago, C++. So it may help to approach the language with a fresh mind.
Yes, coherence is painful. I seem to hit coherence issues pretty rarely myself, but they're annoying when they happen. There's work going on to improve these pain points (specialization!), but it's overall not that big a problem.
I think an example of what you tried might help. Preferably a more holistic example, condensed minimal examples tend to hide instances of the XY problem.
> Disclosure: I don't really know what an "overlapping instance" means in this context.
cases where you have more than one implementation that make sense for a given call. C++ solves this by allowing things to overlap and introducing overload resolution rules. Rust solves this by not allowing overlap.
> but I'm guessing the memory foot print would be at least twice what it is in C++. (I should measure that though.)
Go does make extensive use of the stack and has decent escape analysis, so it might not be that bad, really. But measuring it is probably the best way to go here.
> Some other time it's because the standard library doesn't have a trait for a commonly implemented method, so I can't write a generic function to call that method.
Do you have examples? In some cases generic methods are outside the stdlib in different crates, e.g. num_traits.
- civility 10y ago>> It was a weekend learning project, and all I wanted to do was create a freshman level data structure from scratch. > I'm very interested in making such things more accessible At that point, I simply wanted to implement my own version of something like a statically sized matrix to learn how to manage low level memory. Doing a growable array the right way in C++ involves placement new, explicit destructors, std::move and/or std::swap (and exception safety is a bitch), so I'll agree it would be unfair to expect implementing Vec to be simple. However, you can get a rudimentary matrix working almost trivially. I eventually ended up abusing the unsafe code in Vec to get what I wanted: fn alloc<T: Clone + Default>(size: usize) -> Box<[T]> { return vec![Default::default(); size].into_boxed_slice(); } #[derive(Debug)] pub struct Matrix<T> { pub rows: usize, pub cols: usize, pub data: Box<[T]>, // initialized by alloc() above } For what it's worth, I would really prefer the equivalent of this C++: template<class T, long rows, long cols> struct Matrix { // data lives on the stack // the type knows the sizes T data[rows][cols]; }; But that will have to wait for type level integers. > It seems like you're using generics the way you use templates for metaprogramming in C++. I'm not opposed to TMP in C++, but I really only use it in C++98 to make up for the lack of decltype. In C++11, I don't really need it for what I do. Maybe you and I consider TMP to be different things, but I don't think I was trying to do TMP in Rust. I talked with @steveklabnik about this one already: fn simplified_example<T>(a: T, b: T) -> T where T: Copy + PartialOrd { if a.abs() < b.abs() { // some irrelevant math goes here } else { // slightly different math goes here } } What I learned is that I would need to write my own Abs trait to make this work, and that I would need a Cos, Sin, Exp, Log, and many others to make other similar generic functions work. (I've had a good look at the num crate, and I think it got a lot of things wrong, so that's not an attractive option.) This at least has a work around, but it's tempting to just rest on macros. > [regarding coherence] I think an example of what you tried might help I think it's completely broken that this is ok: impl<T: Mul<T, Output=T> + Copy> Mul<T> for Matrix<T> while this is not: impl<T: Mul<T, Output=T> + Copy> Mul<Matrix<T>> for T I've read an explanation or two, but this is a binary operator! Why does the order of the arguments matter for coherence here? I can write a macro to work around this, but maybe operator traits shouldn't be restricted with the same logic as other traits. > There's work going on to improve these pain points (specialization!) You've mentioned that before (at least I think it was you), around the time I was looking at this. I thought specialization had already been accepted, but I haven't stayed all that current. Regardless, how would it help with my binary operators example above (or the generic function above that)? Anyways, these are all really simple things in C++, and they aren't simple in Rust, and that was my main point a few comments back.