Live data from Hacker News

Two Years of Rust

borretti.me

41–50 of 69 posts

Re: Two Years of Rust

#41

Earlier quoted context omitted.

I agree, there are only superficial similarities. Like they're all 3 C-based. And Go and Rust both compile to machine code. I believe once one of the creators of Go mentioned that it felt to some users "like a faster Python". But I have no clue how Python relates to Rust in any sense, I fail to see any similarities. In fact, I would almost be inclined to say that Python and Rust have more differences than similaritie…

> I would almost be inclined to say that Python and Rust have more differences than similarities. This is somewhat of a stretch: dyn Traits in Rust are sort of like compile time duck typing. OTOH, interfaces in Go and virtual functions in C++ are the same thing.

It really depends on what kind of axis you’re talking about. Under the hood, Go’s interfaces and Rust’s “dyn types” (the new nomenclature for trait objects) are the same, and C++’s virtual functions are different.

(Though you can emulate them with unsafe in Rust, like anyhow)

Re: Two Years of Rust

#42

Gripes about the borrow checker I think would be cured with the following surprising fact, and interesting "problem solving" approach to the language's design: In Rust there's at least 5 types of everything, in order of strength: - Value / unqualified / "owned" - Generically, T - Optionally mutable - Mutable Reference - &mut T - you can only have one of these for a given value - Reference / Shared reference - &T - yo…

I feel like it's intuitive for me to think about this stuff as just a second type system rather than to think about the details of how the compiler works or how it'll function at runtime. A given value exists in a kind of superposition, and I pick the form I want it to collapse into (value, reference, mutable reference, etc) based on the tradeoffs I need at that moment. I don't know exactly why this is helpful, or if…

The only time this gets into trouble is when you bridge the &/&mut and *const/*mut worlds. If you need the latter, try to stay within them as much as possible, if not entirely.

Re: Two Years of Rust

#43
post #8
post #3

My biggest issue with rust after two years is just as you highlight: the mod/crate divide is bad! I want it to be easier to have more crates. The overhead of converting a module tree into a new crate is high. Modules get to have hierarchy, but crates end up being flat. Some of this is a direct result of the flat crate namespace. A lot of the toil ends up coming from the need to muck with toml files and the fact that…

It's a surprising choice that Rust made to have the unit of compilation and unit of distribution coincide. I say surprising, because one of the tacit design principles I've seen and really appreciated in Rust is the disaggregation of orthogonal features. For example, classical object-oriented programming uses classes both as an encapsulation boundary (where invariants are maintained and information is hidden) and a d…

The history here is very interesting, Rust went through a bunch of design iteration early, and then it just kinda sat around for a long time, and then made other choices that made modifying earlier choices harder. And then we did manage to have some significant change (for the good) in Rust 2018.

Rust's users find the module system even more difficult than the borrow checker. I've tried to figure out why, and figure out how to explain it better, for years now. Never really cracked that nut. The modules chapter of TRPL is historically the least liked, even though I re-wrote it many times. I wonder if they've tried again lately, I should look into that.

> Another example is the trait object (dyn Trait), which allows the client of a trait to decide whether dynamic dispatch is necessary, instead of baking it into the specification of the type with virtual functions.

Here I'd disagree: this is separating the two features cleanly. Baking it into the type means you only get one choice. This is also how you can implement traits on foreign types so easily, which matters a lot.

Re: Two Years of Rust

#44
post #31
post #8

Earlier quoted context omitted.

It's a surprising choice that Rust made to have the unit of compilation and unit of distribution coincide. I say surprising, because one of the tacit design principles I've seen and really appreciated in Rust is the disaggregation of orthogonal features. For example, classical object-oriented programming uses classes both as an encapsulation boundary (where invariants are maintained and information is hidden) and a d…

Hard agree. Is retrospect I think the model of Delphi, where you must assemble `manually` a `pkg` so you can export to the world should have been used instead. It also have solved the problem where you ended doing a lot of `public` not because the logic dictated it, but as only way to share across crates. It should have been all modules (even main.rs with mandatory `lib.rs` or whatever) and `crate` should have been a…

> Hard agree. Is retrospect I think the model of Delphi, where you must assemble `manually` a `pkg` so you can export to the world should have been used instead.

Very very old Rust had "crate files" which were this https://github.com/rust-lang/rust/blob/a8eeec1dbd7e06bc811e5...

.rc standing for "rust crate"

There's pros and cons here. I'm of two minds.

Re: Two Years of Rust

#45
post #12

> The way I would summarize Rust is: it’s a better Go, or a faster Python That's an interesting take. I feel like all three of these languages fit into pretty discrete lanes that the others don't. Python for quick hacking or scientific stuff, Go for web services and self-contained programs, Rust for portability (specifically sharing code as C ABI or WASM) and safety. > It’s not hard to learn I agree Rust is easy to l…

Python requires less lines of code (much less?). Comparing Ruby with Python wouldn't shock me.

Really depends on what you're doing, and how you write the Rust. I'd never claim this is always true, but Rust can be pretty concise depending on what you're doing. See this previous comment of mine, which links to some others with an example I've evolved over time on this forum: https://news.ycombinator.com/item?id=42312721

Re: Two Years of Rust

#46
post #15

> Error Handling I've yet to see anyone demonstrate the elegance Rust error handling for anything but the simplest of cases. It's all fun and games and question marks... until you hit this: $ ./app called `Result::unwrap()` on an `Err` value: no such file or directory And then you start investigating and it turns out that the error value comes from somewhere deep in an unknown callstack that got discard by the author…

?ing Errors and never actually handling them is just a terrible practice. In fact it is just as bad as not doing error checking at all. Misusing a mechanism is not a point against the language. What makes this error checking good is that you can use it correctly and it is less cumbersome than the try/catch from C++. >Come on, let's be a bit more honest with ourselves about Result and '?' - it's not a full solution to…

> ?ing Errors and never actually handling them is just a terrible practice.

This isn't true. It really depends.

    fn main() -> anyhow::Result {
can be perfectly good, depending on your needs.

What Rust does with error handling is give you flexibility. It's true that means you can make a mess. I myself have a TODO on my current codebase where I'm not exactly happy with what I'm doing at the moment overall. But it can also be very elegant, and more importantly, it doesn't force you into one paradigm that many not be good for your needs, but allows you to decide, which is very important in Rust's conceptual space. I wouldn't want to be forced to use the above signature in a no_std context, for example.

Re: Two Years of Rust

#47
post #13

Use dependency injection and mock behaviors. This technique works in several programming languages, including Rust. Rust has modules, crates and workspaces. To optimize builds, you'll eventually move shared resources to their own crate(s).

I feel in rust you want to be a lot more judicious in where you introduce and deal with traits than in other languages with interfaces. Author blames lifetimes for this but I think the truth is that it is because there is no garbage collector so not everything is a fat pointer and fat pointers cannot have generic methods anyways because generic methods are monomorphized so they feel a bit lame even if you would reach…

Here's how I currently am doing it: I use the repository pattern. I use a trait:

  pub trait LibraryRepository: Send + Sync + 'static {
      async fn create_supplier(
          &self,
          request: supplier::CreateRequest,
      ) -> Result;
I am splitting things "vertically" (aka by feature) rather than "horizontally" (aka by layer). So "library" is a feature of my app, and "suppliers" are a concept within that feature. This call ultimately takes the information in a CreateRequest and inserts it into a database.

My implementation looks something like this:

    impl LibraryRepository for Arc {
        async fn create_supplier(
            &self,
            request: supplier::CreateRequest,
        ) -> Result {
            let mut tx = self
                .pool
                .begin()
                .await
                .map_err(|e| anyhow!(e).context("failed to start SQLite transaction"))?;
    
            let name = request.name().clone();
    
            let supplier = self.create_supplier(&mut tx, request).await.map_err(|e| {
                anyhow!(e).context(format!("failed to save supplier with name {name:?}"))
            })?;
    
            tx.commit()
                .await
                .map_err(|e| anyhow!(e).context("failed to commit SQLite transaction"))?;
    
            Ok(supplier)
        }

where Sqlite is

  #[derive(Debug, Clone)]
  pub struct Sqlite {
      pool: sqlx::SqlitePool,
  }
You'll notice this basically:

  1. starts a transaction
  2. delegates to an inherent method with the same name
  3. finishes the transaction
The inherent method has this signature:

  impl Sqlite {
      async fn create_supplier(
          self: &Arc,
          tx: &mut Transaction,
          request: supplier::CreateRequest,
      ) -> Result {
So, I can choose how I want to test: with a real database, or without.

If I want to write a test using a real database, I can do so, by testing the inherent method and passing it a transaction my test harness has prepared. sqlx makes this really nice.

If I'm testing some other function, and I want to mock the database, I create a mock implementation of LibraryService, and inject it there. Won't ever interact with the database at all.

In practice, my application is 95% end-to-end tests right now because a lot of it is CRUD with little logic, but the structure means that when I've wanted to do some more fine-grained tests, it's been trivial. The tradeoff is that there's a lot of boilerplate at the moment. I'm considering trying to reduce it, but I'm okay with it right now, as it's the kind that's pretty boring: the worst thing that's happened is me copy/pasting one of these implementations of a method and forgetting to change the message in that format!. I am also not 100% sure if I like using anyhow! here, as I think I'm erasing too much of the error context. But it's working well enough for now.

I got this idea from https://www.howtocodeit.com/articles/master-hexagonal-archit..., which I am very interested to see the final part of. (and also, I find the tone pretty annoying, but the ideas are good, and it's thorough.) I'm not 100% sure that I like every aspect of this specific implementation, but it's served me pretty well so far.

Re: Two Years of Rust

#48

"The two areas where it’s not yet a good fit are web frontends (though you can try) and native macOS apps." Could you please elaborate?

At Oxide, we named the company after Rust, but we use TypeScript on the frontend, not Rust. Rust is our default technology choice for most new code, but TypeScript is for frontend code.

Rust on web frontends is just not super mature. You can do things with it, and it's very cool, but TypeScript is a very mature technology at this point, and gives a lot of similar benefits to Rust. And it can live natively in a browser context, without complex bindings.

I don't work on native macOS apps, but I'm assuming it's similar: Objective-C or Swift are expected, so you end up needing to bind to APIs that aren't always natural feeling. I could see why you'd want to do something similar: write your core in Rust, but make the UI stuff be in Swift, and call into it from there.

Re: Two Years of Rust

#49

Earlier quoted context omitted.

I feel in rust you want to be a lot more judicious in where you introduce and deal with traits than in other languages with interfaces. Author blames lifetimes for this but I think the truth is that it is because there is no garbage collector so not everything is a fat pointer and fat pointers cannot have generic methods anyways because generic methods are monomorphized so they feel a bit lame even if you would reach…

Here's how I currently am doing it: I use the repository pattern. I use a trait: pub trait LibraryRepository: Send + Sync + 'static { async fn create_supplier( &self, request: supplier::CreateRequest, ) -> Result ; I am splitting things "vertically" (aka by feature) rather than "horizontally" (aka by layer). So "library" is a feature of my app, and "suppliers" are a concept within that feature. This call ultimately t…

Thanks so much for the detailed example. Bookmarking for when I need to find it again...

Re: Two Years of Rust

#50
post #49

Earlier quoted context omitted.

Here's how I currently am doing it: I use the repository pattern. I use a trait: pub trait LibraryRepository: Send + Sync + 'static { async fn create_supplier( &self, request: supplier::CreateRequest, ) -> Result ; I am splitting things "vertically" (aka by feature) rather than "horizontally" (aka by layer). So "library" is a feature of my app, and "suppliers" are a concept within that feature. This call ultimately t…

Thanks so much for the detailed example. Bookmarking for when I need to find it again...

No problem! I also left this response to someone on reddit who said similar:

Nice. I want to write about my experiences someday, but some quick random thoughts about this:

My repository files are huge. i need to break them up. More submodules can work, and defining the inherent methods in a different module than the trait implementation.

I've found the directory structure this advocates, that is,

    ├── src
    │   ├── domain
    │   ├── inbound
    │   ├── outbound
gets a bit weird when you're splitting things up by feature, because you end up re-doing the same directories inside of all three of the submodules. I want to see if moving to something more like

    ├── src
    │   ├── feature1
    │   │   ├── domain
    │   │   ├── inbound
    │   │   ├── outbound
    │   ├── feature2
    │   │   ├── domain
    │   │   ├── inbound
    │   │   ├── outbound
feels better. Which is of course its own kind of repetition, but I feel like if I'm splitting by feature, having each feature in its own directory with the repetition being the domain/inbound/outbound layer making more sense.

I'm also curious about if coherence will allow me to move this to each feature being its own crate. compile times aren't terrible right now, but as things grow... we'll see.

Post reply on HN