I've been dabbling in Rust on and off since 2014, and this has always been my feeling. I thought my background in C++ would make things reasonably easy, and while I have little fondness for C++, it's still easier to get things done than with Rust, which is not at all what I expected given my 4 years with the language. There's a lot to like about Rust, but it falls far short of the "easy-as-Go" promises made by many o…
My impression is that people use Rust for correctness, security, and speed over ease of use. Although I do think once you get over the initial learning curve (which is admittedly large) it mostly makes sense.
100 days with Rust: a series of brick walls
151–160 of 323 posts
Re: 100 days with Rust: a series of brick walls
#152The popularity of Rust and its friendly hand-holdy docs almost don't even make sense for what it is, to me anyway. I suspect it will end up frustrating many high-level developers.
Re: 100 days with Rust: a series of brick walls
#153Earlier quoted context omitted.
> It's important to note that in C++ you can easily run into problems with what you're describing: structs (objects) with different sizes being put into a vector. This is simply wrong. > You may very well get the program to compile, but then run into odd bugs which come about because your objects get clipped to the size of the smallest possible (the base class). That has nothing to do with object sizes. You're descri…
Okay, sure, if you want to get into the nitty gritty, that's fine. But I just wanted to provide a high level overview of how putting differently-sized structs into a vector can be problematic. Sure, ignorance, whatever. Indeed. That's why I brought it up. But my greater point still stands: do you want the compiler to catch these things? If so, you have trade-offs.
A vector is a container for homogeneous sequences of fixed-size objects that are stored contiguously. Even if we ignore the slicing problem, don't you see a problem in trying to shove sets of square and rectangular pegs into a container that was specifically designed to support only round pegs of one specific size?
Re: 100 days with Rust: a series of brick walls
#154There are a few things in life you just should not do. One of them is jump from your only language experience being Python into an advanced language like Rust when you’re struggling to grok SQL.
Brandur is not struggling with SQL[0][1]. That was an illustrative example of the kinds of walls you encounter in life as a programmer. [0]: https://brandur.org/postgres-reads [1]: https://brandur.org/postgres-atomicity
Re: 100 days with Rust: a series of brick walls
#155Earlier quoted context omitted.
Yeah. So I'm following a ray tracing book that uses C++ as example code, and am using Traits as a form of interface: there is a trait of Material that has a function on it, and then there are different "implementations" of Material with different implementations of that fn, and can have arbitrary data stored against them. I need to have them sized because I need to copy them at a point where I only understand they ar…
Would it work to add a boxed_clone() function to your Material trait that returns a Box ? (or create a BoxedClone trait if you need that pattern more generally) Implementations of boxed_clone() could still use &self.clone() to limit boilerplate. Alternatively, if the Materials are not going to be modified and you just need multiple references to the same object, you could replace the Box with an Rc or Arc .
Re: 100 days with Rust: a series of brick walls
#156Earlier quoted context omitted.
(Responding to myself to reply to the parent's edit) > You're absolutely right. What I really meant was that you can't have Vec where T is a trait without also wrapping that in a box, which for me in turn doesn't work for a bunch of other reasons. Ah, it sounds like you're using traits as types directly, which is very much discouraged by Rust ( especially in conjunction with taking references to those traits). What t…
> Ah, it sounds like you're using traits as types directly, which is very much discouraged by Rust (especially in conjunction with taking references to those traits). What the language really prefers for you to do is to use traits as bounds on generic types Can you write an example?
// Define two different types
struct Chihuahua;
struct GreatDane;
// Define a trait with a method
trait Bark {
fn bark(&self);
}
// Implement that method for both types
impl Bark for Chihuahua {
fn bark(&self) { println!("woof") }
}
impl Bark for GreatDane {
fn bark(&self) { println!("WOOF") }
}
Using instances of these types looks like so: let rover = Chihuahua;
let marmaduke = GreatDane;
rover.bark(); // woof
marmaduke.bark(); // WOOF
Now say that you want to write a function that accepts any type that implements the Bark trait. As I mentioned before, there's two ways to do it: the static way, and the dynamic way. Here's what both versions of the function look like: fn speak_static(dog: T) {
dog.bark();
}
fn speak_dynamic(dog: Bark) { // wait for it...
dog.bark();
}
In the first one, there's a generic type (the "T"), which we have bounded by the `Bark` trait. At compile-time, for each different type that you use with this function it will generate a new copy of the function with "T" replaced with whatever type you actually used (this might seem excessive, but it's crucial for further optimizations).Furthermore, calling this function is trivial:
speak_static(rover); // woof
speak_static(marmaduke); // WOOF
The fact that it's so easy to use these functions is what we mean when we say that Rust "prefers" static dispatch. I'll come back to this in a moment.For the dynamic version, it's different because there's no generics at all. Instead, the function is just taking a normal parameter of type `Bark`. Looks simple, right? In fact, it even looks simpler than the static version! The illusion of simplicity is what makes this so pernicious to beginners. In fact, I've lied to you completely: despite seeming like this should work, it doesn't even compile. That's because, unlike many other languages, Rust doesn't heap-allocate (or "box") things by default. It has to pass function parameters, unboxed, on the stack. And trying to generate a single version of a function whose parameters have unknown size is pretty fundamentally unsafe.
So we have to give this parameter a size. If you're coming from a high-level language, even this is already probably an alien concept (especially since "size on the stack", which is what we care about here, isn't the same thing as "total size of every memory allocation this type might transitively point to").
Anyway, we give this type a size by sticking it behind a pointer. There are many different pointer types we can use depending on one's need. The simplest is probably `Box`:
fn speak_dynamic_box(dog: Box) {
dog.bark();
}
Of course, using a `Box` implies a heap allocation, and, since Rust loves speed, it also loves to prefer stack allocation to heap allocation. So what you might actually want to do instead is use a reference, which will let you avoid the heap altogether: fn speak_dynamic_ref(dog: &Bark) {
dog.bark();
}
Now you have a function that takes a stack-allocated reference to a stack-allocated vtable. There's still two pointer indirections to calling `bark()`, which isn't great, but at least we've gotten rid of that heap allocation.It doesn't end there, though. If you try to just call `speak_dynamic_box(marmaduke)`, which is how easy it was for `speak_static`, the compiler will error. That's because `speak_dynamic_box` doesn't take a `GreatDane`, it takes a `Box`, which isn't even close to the same thing. So you have to call it like this:
speak_dynamic_box(Box::new(marmaduke) as Box);
Not only do you have to box it up manually, but you have to cast it into a trait object. Not pretty, and definitely not worth avoiding generics for.And all this is still understating the restrictions on trait objects. For example, once you cast to a trait object, you can't cast back to the original type (the original type is lost, and if we let you cast back then you'd be able to turn `rover` into a `GreatDane`!). Furthermore, because of various inherent restrictions to how vtables work, not all traits can even be used as trait objects (and trying to explain the technical justification behind these rules, known collectively as "object safety", is enough to make anyone's eyes glaze over). Furthermore, getting back to the `speak_dynamic_ref` example, this only looks as simple as it does (and it doesn't really look simple) because of how simple our example is. If you try to expand this example into anything useful, then you quickly need to really know what you're doing with lifetimes lest you fall into despair.
To summarize, trait objects are an advanced feature that should only be attempted by people who need dynamic dispatch. Rust is designed to favor static dispatch. Don't be fooled by the apparent simplicity of defining functions or structs that take traits as types. In fact, in the near future we'll be introducing a new keyword to make it absolutely clear when trait objects are being used, solely so that new users don't fall into the trap of thinking that they're a simpler path forward than generics.
Re: 100 days with Rust: a series of brick walls
#157As someone who has been programming in Rust for nearly a year, even for commercial purposes, this article is baffling to me. I've found the compiler messages to be succinct and helpful. The package system is wonderful. It's dead easy to get something off the ground quickly. All it took was learning how and when to borrow.
I can see where the author comes from. I've been working with ^W^W fighting against Tokio this week, and the error messages are horrible. Representative example: error[E0271]: type mismatch resolving ` + std::marker::Send>, [closure@src/server/mod.rs:59:18: 59:74]>, [closure@src/server/mod.rs:60:19: 69:10 next_connection_id:_], std::result::Result >, futures::MapErr , [closure@src/server/mod.rs:74:18: 74:74]>>, std::…
This is so awesome.
Re: 100 days with Rust: a series of brick walls
#158There are a few things in life you just should not do. One of them is jump from your only language experience being Python into an advanced language like Rust when you’re struggling to grok SQL.
So what do you propose? He learn C first? Or he's just not allowed to learn Rust?
Re: 100 days with Rust: a series of brick walls
#159Earlier quoted context omitted.
Dynamic dispatch will surely always be faster to compile than static dispatch, but static dispatch has crucial runtime performance advantages (e.g. it enables inlining, which is the ultimate meta-optimization). And extreme runtime performance is one of Rust's raisons d'etre. Having fast compilation is surely a worthwhile goal, but, for Rust, not if it comes at the expense of runtime performance. What do you mean by "…
> What do you mean by "prevent reusability"? I read that as being about reusing the implementation - the actual machine code bytes which encode a function. If i call a function in library which uses dynamic dispatch, the compiler just emits a few instructions to jump to an existing implementation in the library. If i call a function in a library which uses static dispatch, the compiler will include the relevant monom…
Indeed, and that's what precisely one wants if they've chosen static dispatch since the alternative approach is opaque and inhibits optimization. Inlining across compilation units is a rather important feature! I don't see how that prevents reusability, though?
Re: 100 days with Rust: a series of brick walls
#160I decided to spend a day and build 3 versions of a basic Usage block to get a feel for each. I'd never written Go or Rust, and only passable C.
I did C first and then tried Rust. I gave up. It was too much of a learning curve for a ~3 mos project. I ended up using Go. I'll write up a blog post soon, Go is not without challenges, but I'm comforted by blog posts like this to see that I'm not the only mortal here still challenged by Rust.