Hacker News new | ask | show | jobs
by idubrov 1954 days ago
I have some extensive (self-assesment :) ) experience building this kind of application, and my answer would be "no, but maybe" (I was one of the first engineers & architect on our zero to ~500k codebase).

Some of the arbitrary, random things I've learned:

1. Given how nice and powerful language is, Rust works relatively well with less experienced engineers. Potentially. You can build very nice APIs which are straightforward to use.

If you can stay in this territory, everything is great.

However, there are some hard walls in Rust which are really difficult to jump over. And once you hit them, you really need somebody who understands Rust really really well and is capable of working around those issues.

2. It was quite hard to find the right balance between engineering time, compilation/linking time, safety, ease of use, performance, etc. Might be my personal biases, but I had to make some questionable decisions to keep compilation times / turnaround times at bay (like, unsound plugin system, custom serialization framework or test framework using nightly Rust features).

3. The type system is really, really nice. This alone compensates for a lot of things.

4. Ecosystem? Simply amazing. High quality libraries, documentation and everything.

5. Ecosystem again? Lots of things are still missing.

6. Performance? I'm 90% trolling here, but my experience was that Rust is not "blazing fast" by default. Not for "enterprise" software. You have to do some legwork sometimes. I've built some simple tool to do certain transformation between JSON and XML, and out of gate it was ~2x slower than Java equivalent (yes, with release build). Turned out, strings are not that cheap to clone if all you have is a bunch of strings. I did make it like 5x times faster than Java in the end, but it did require some weird tricks (like forcing hash map to look for a "derived" key than I give it).

There were some other cases where performance was reduced by simple things (like, having "heavy" Result vs having error variant boxed).

I think, this is "easily" counteracted by adopted practices and libraries (optimized error types, for instance), some standard patterns (like don't be afraid of Arc, they are better than to move huge data chunks around), maybe, good profilers as well.

This is probably also my biases talking here, frankly, I don't think it mattered at all (performance was killed by database, as is very common with enterprise systems). Also, in the end, it was fast (outside of database woes).

7. Overall, I find Rust very exciting language to work with, which was a big driver (but that doesn't necessarily scale -- you'll have to have some answer prepared when less experienced engineers will ask you "but why can't I do like I did in Typescript in those trivial 500 lines of code").

Would I do it again? Probably, but with understanding that whoever pays for it, might be paying for my "fun" on top of the product they are getting. Which is not necessarily a bad thing -- "fun" is also a factor in attraction and retention of engineers.

I'm also not going to lean into the "dark side". Like, if all you care is to get some half-broken whatever out as quick as possible, Rust might not be the right choice. It makes you think about "right or wrong" a lot, imo.

1 comments

> Turned out, strings are not that cheap to clone if all you have is a bunch of strings.

Yeah, if you're doing a lot of cloning, you'll probably run into performance issues at some point. A common way to solve that problem is usually to use references instead of cloning.

Of course, writing your code that way takes more work/thought/planning.

>A common way to solve that problem is usually to use references instead of cloning.

Right. I think, we ended up with having everything from the list:

1. String 2. &str 3. Cow<str> 4. Arc<str>, for interned strings (thin Arc would be even better & there is probably a crate for this) 5. Something like owning_ref::ArcRef<Owner, str> 6. One-off tricks where you actually need to construct a new string, but don't want to really construct it (for example, for hash lookup).

#5 I think is undervalued, actually; it's amazing for "enterprise" kind of stuff where you have large trees of data you need to pass around & you don't want to use straight borrowing (like &'a Whatever) because lifetimes are too infectious. And you don't want to use Arc at every corner (like, say, Java would do, not quite, but in semantics).

My problem, though, was to explain all the nuances given that they usually have nothing to do with the "business" part of the problem somebody was solving.

> My problem, though, was to explain all the nuances given that they usually have nothing to do with the "business" part of the problem somebody was solving.

Yeah, I completely agree. A GC provides a lot of benefits in terms of clarifying the intention of business logic.

Unless, of course, performance/memory usage is an important part of your business logic, in which case Rust is exactly what you want.

Maybe I'm weird, but I find the explicit ownership, mutability and lifetime information encoded in Rust definitions very helpful at understanding a new code-base. Something that otherwise needs to be documented in comments / external documentation in Java, but most often it is missing, and then recovering such information from the code is similarly hard to recovering the types in a dynamic language.

Lack of GC in makes it harder to write but easier to read.

It gives you a lot more information, but a lot of that information is more about lower-level mechanics than "business logic". I think it really depends on what you're writing and what's important to it.
It can be actually tied pretty well to business logic. You can explicitly model some business rules, e.g. a subscription cannot exceed the lifetime of the user account, etc. Similar to how you can use static type system to prohibit invalid states. Here Rust gives more tools of this kind, than other languages.

Another one I really love is ability to destroy objects on final operation. E.g you close something and it can't be used any more. Most other languages can protect using such closed object only with runtime exceptions.

Some languages like Java don't even make a distinction between "object A is composed of B and C" vs "uses B and C" (in both cases they'd be references)

Or, to get the computational equivalent of what Java is doing (immutable, interned strings), use a Rust string interning library like what servo uses [1], or just `Arc<str>`

[1]: https://docs.rs/string_cache/