Live data from Hacker News

100 days with Rust: a series of brick walls

brandur.org

151–160 of 323 posts

Re: 100 days with Rust: a series of brick walls

#151
post #54

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.

That's the hope, but I'm 4 years in and still climbing that curve... :/

Re: 100 days with Rust: a series of brick walls

#152
As a long-time C guy, it's nice to see Rust as a modern replacement. But most of the time, such low level programming is not needed.

The 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

#153

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

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

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

#154

There 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

Then perhaps he should become a fictional writer then. So what parts of this “story” are even true? But more importantly, what is the point of writing a fictitious story like this? If he’s not struggling with SQL and Rust, then what is it? Clickbait? I think you mean an illustrative example of Rust-bashing.

Re: 100 days with Rust: a series of brick walls

#155
post #126

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

Yep, those are the best approaches. For the former, you might consider the 'objekt' crate, which avoids the boilerplate:

https://docs.rs/objekt/0.1.0/objekt/

Re: 100 days with Rust: a series of brick walls

#156
post #84

Earlier 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?

Sure thing. [Preemptive postscript: damn this got long. TL;DR don't use trait objects, just use generics. You'll thank me.] Here's the setup: we have several different types, and those types implement the same trait (think of it like an interface from other languages).

    // 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

#157
post #10

As 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::…

The last tokio re-ergo really helped me and my classmate.. now we are getting stuff done, and we are enjoying it a lot, since we are combining our connections as instances of async state-machines (which, by the way, a given state may be a sub state-machine).

This is so awesome.

Re: 100 days with Rust: a series of brick walls

#158
post #119

There 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?

When did I say he wasn’t allowed to learn Rust?

Re: 100 days with Rust: a series of brick walls

#159
post #128
post #116

Earlier 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…

> If i call a function in a library which uses static dispatch, the compiler will include the relevant monomorphisation of the function, and any other generic functions it calls.

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

#160
I consider myself a good enough engineer, and I've picked up a lot of languages. Recently I built and launched a server agent for Cronitor. Going in I knew it needed to be portable and compile to an executable. I considered Go, Rust and C.

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

Post reply on HN