I'm not anti rust in any way, it's more of having limited time and so many languages problem. But I'm interested in the types of problems rust is good for, so I could consider it if/when I encounter them
I'm not anti rust in any way, it's more of having limited time and so many languages problem. But I'm interested in the types of problems rust is good for, so I could consider it if/when I encounter them
- Rust is naturally pretty fast, on the rough order of C++ in many cases. Even unoptimized programs tend to be very snappy (as long as you remember to compile in release mode and buffer I/O).
- Rust tends to warn me if I make a dumb mistake, rather than having my program mysteriously corrupt memory at run-time.
- Rust has very nice support for portable CLI applications, both in the standard library and in third-party libraries.
- With a bit of extra work, I can usually deliver a single, statically-linked Linux binary. Go is even better at this, but Rust does it well.
- It turns out the "algebraic data types", what Rust calls "enum" and "match", are just a really nice way to write down every possible "case" or "state" of some value. If a piece of software involves hundreds of special cases, Rust allows me to think about them clearly.
- Rust has surprisingly good third-party libraries for many things. (Not everything.)
- Multithreaded Rust apps are a dream.
Cons:
- Rust forces me to keep track of whether things live on the heap or the stack, whether I'm passing them by value or reference, and so on. This is good when I want performance, but for other kinds of code, it's just a "cognitive tax."
- Rust's learning curve is higher than Go, but arguably lower than C++. This is especially true if you've either never worked with stack/heap/pointers before.
- Rust tends to favor "mostly functional" architectures over "mutable objet soup" architectures. This pushes you to use less-familiar designs for games and GUI libraries, for example.
How is that a con?
The most popular pure rust ones are based on somewhat esoteric patterns and seem prone to being abandoned.
Within that there are some interesting options, like for functionally reactive programming and immediate mode guis, but these paradigms aren’t all encompassing and in fact cover a pretty minority use case based on what GUIs are being created today.
Maybe one day we’ll all only ever create purely functional GUIs and speak Esperanto, but until then, we’re a little hamstrung in rust.
On the upside, a lot of C++ game programming uses an ECS-style architecture, which does work well in Rust.
I wanted to write some helper functions that applied an argument to a closure and I got this code
fn apply<A, B, C, G>(mut f: impl FnMut(B) -> G, a: A) -> impl FnMut(&B) -> C
// must still be `for<'r> impl FnMut(&'r B) -> C`, because that’s what filter requires
where
G: FnMut(A) -> C,
B: Copy, // for dereferencing
A: Clone,
{
move |b| f(*b)(a.clone()) // this must do any bridging necessary to satisfy the requirements
}
needless to say, Rust doesn't do automatic currying or any advanced functional language tricks so it's just painful to write the same type of codeBut if we zoom in from the architecture to the individual lines of code, Rust is more of a mix of functional and imperative styles. Rust's iterators can be fairly pleasant for working with lazy streams. Closures are more of a mixed bag. This is partly because closures need to worry about ownership, which complicates things by splitting closures up into Fn, FnOnce and FnMut. And each closure is a separate anonymous type. So, yeah, Rust supports quite a few useful things with closures. But as your example shows, the type declarations can quickly become intolerable.
So my strategy is to go with the flow, and keep things simple. I only bring out the heavy type declarations on special occasions, when they provide a big payoff. Otherwise I keep things as simple and boring and concrete as I can.
There are a few popular Rust libraries which make very heavy use of generics. I find that this sometimes backfires, making those libraries slower to compile and harder for me to understand.
I tried to learn Rust when I only knew Python and had not CS education, I gave up rather quickly. Then I did some projects in C and later on C++, and now I understand Rust. Because I understand what are the problems it tries to solve. Imho only after you have some skills in C++, and/or some kind of CS education, can you really appreciate what Rust is about.
Any examples?
Ways in which it is better:
* A decent package manager
* "Safe by default" (you have to use 'unsafe' to turn off the safety features), unlike C++ which is "unsafe by default" (v[i] is undefined behaviour if i is out of bounds, for all of C arrays, std::arrays and std::vectors).
* Really easy to do multithreading -- I had reached the point with C and C++ where I was of the opinion it is "almost impossible" to write large thread-safe programs, and all communication should be between processes with pipes, wherever reasonable. In Rust I again feel I can safely and productively write multi-threaded code.
The things Rust doesn't let you do (throw pointers around everywhere) can get a little annoying, but I find the extra safety increases my productivity enough that it's worth the tradeoff.
"unsafe" doesn't turn off any safety features, it just allows you to do additional things which the base language does not, like:
* use raw pointers
* use mutable static globals
* access fields of unions
https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#unsa...
All the rules of safe Rust still apply inside of unsafe blocks. The borrow checker still works, etc.
v[i] is bounds checked in Rust even in unsafe. The unsafe "super powers" don't include "magically now indexing has no bounds checks" but because Rust's get function v.get_unchecked(i) is marked unsafe‡ you would need to mark code unsafe to use that function and that function doesn't have bounds checks.
If you write:
unsafe {
println!("{}", v[i]);
}
The compiler will point out that unsafe isn't doing anything for you here, v[i] is safe, marking it unsafe is just a waste of everybody's time.C++ doesn't have bounds checked array access for its abandoned built-in arrays, but it does (these days) have bounds checked access for std::array and std::vector as a separate member function, however because they aren't the default few C++ programmers use them even where they undoubtedly should. This is an ergonomics issue, it's just easier to write needlessly unsafe C++.
‡ This said just get() before I edited it, but Steve corrected me, see below.
> because Rust's get function v.get(i) is marked unsafe
You mean get_unchecked. get is safe, and returns an Option, so that None can be returned if something is out of bounds.
Most devs should learn to use profilers.
This is also true in Rust for the same reason, arguably more true since Rust's built-in array type knows how big it is - but the observation in both languages programmers who want indexing write v[i] because that's the natural way to express what you meant, what's different isn't the frequency of need, only the language defaults.
> performance is such that you'd never want that enabled for release.
Let's make sure it's correct before we start worrying about performance
The OS parts need to run on bare metal and do all sorts of cursed shenanigans, where Rust's separation of safe and unsafe code lets us better focus on proving to ourselves that the unsafe bits are working as intended (we also test our unsafe code under Rust's Miri interpreter/sanitizer for extra confidence). IOW, the killer feature is low-level control plus a reasonable expectation of safety.
The actual WebAssembly interpreter is also written in Rust, which is a good fit because inefficiency in an interpreter leads to inefficiency in anything running in the interpreter. The killer feature here is performance plus safety.
The overall package is bundled up into a CLI app that lets you deploy an instance of our OS with your application running within. Rust has a fairly nice wealth of libraries that make this pleasant, from CLI argument parsers to cryptography to OIDC and beyond. And it all gets packaged up into a single binary for easy distribution (even the OS and interpreter parts, which get embedded directly in the binary via `include_bytes!`). The killer feature here is great platform support (including cross-compilation) and small binaries (though you may reasonably disagree on "small"; altogether the binary (again, including the OS and interpreter) comes out to 25 MB).
Finally, Rust's lack of a pervasive runtime and great WebAssembly tooling means that it's easy to compile Rust applications to run on top of all this as well.
I also have some side projects like a gameboy emulator, where it's nice to have what is effectively a lower level language with the safety and features of higher level language.
My day job is mostly Java/Javascript though, rewriting the world isn't yet worth it, but there's certainly one or two of our systems where if a rewrite in another language was on the table I'd make the case for Rust
By generously use of copying?
Joking aside, I guess devs are lured by the tooling, performance, single binary output and enthusiastic community (more people can see your projects).
If OCaml, Rust doesn't have weirdly sized number types and has a decent library ecosystem.
If Haskell, you don't need to understand monads to write hello world.
If Scala, you don't need to deploy a JVM with your app.
If F#, you have traits which are super useful and you aren't fighting against MS's refusal to invest in the nicest language in the .Net ecosystem.
Even without "lifetime tracking" and liberal use of Clone, you can still outperform most programs written in any of the above languages.
I think I understand what you're referring to, but stop me if I get this wrong. If you put a bunch of objects in a Vec and refer to them by index, you have to be careful with operations like .insert() and .remove() that shift Vec elements around and change their indexes. Also if you make each element an Option<T> to support deletion without .remove(), you still have to be careful about indexes of deleted objects, because you might put a new object in the same spot later. I have several thoughts about this:
- If "memory safety without garbage collection" is one of the unique features of Rust, you still have that. A reused Vec index in safe Rust code will never corrupt memory or trigger a segfault or anything like that. It's strictly a "logic bug".
- These logic bugs only apply specifically to the objects you put in a Vec and track by index. Creating a Vec<Foo> and getting an index mixed up doesn't change anything about Rust handles your local variable of type Bar (or for that matter, your local variable of type Foo).
- These bugs are understandable for beginners, and you can follow what's going on with print statements. Contrast that with a dangling pointer into a C++ std::vector that just reallocated, where printing the freed object often appears to show good data, and explaining the bug means talking about malloc/new and the heap. Relatedly, unwrapping a None in Rust will always panic and will never coincidentally appear to work. (Your comment was comparing Rust to higher level languages, but I've heard similar comparisons to C++.)
- There are good options for fixing the problem. Beginners might prefer to put their objects in a HashMap with an incrementing key. There's a performance cost to that, but it's familiar and relatively footgun-free. More advanced folks might reach for something like "generational indexes into a slab", which gives you back some performance in exchange for making you learn a bunch of new buzzwords and figure out which implementation to choose from crates.io.
So yes, with those caveats, putting objects into a Vec does turn some compile-time bugs into runtime bugs. That is a downside, much like Rc/Arc/RefCell/Mutex come with runtime downsides. But I've heard folks describe it in terms like "turning off the borrow checker", and I don't think that's a very accurate way of describing it.
The reason why I describe it as "turning off the borrow checker" is because that's exactly what it is - those indices are pointers semantically, but there's no ownership tracking for them. So for them, you turned off the checker. The more you use them, the less checked your code is. If you use this approach in some isolated piece of code, it's one thing. But if it's the go-to solution, then it's reasonable to wonder why you'd do that instead of using a language that has built-in ergonomic safe references with no borrow checking.
Regular Vec indices yes, but not incrementing HashMap keys or other fancier things, if we set aside overflow issues.
> But if it's the go-to solution
Oh yeah, I should clarify this point. Rust definitely prefers to use simple ownership (i.e. the ownership/reference graph is a tree) wherever possible. As an example, say we've got a Python program with Person objects and Dog objects. Each Person has a `pets` collection that might contain some dogs, and each Dog has an `owner` field that points back to their Person. When we port this program from Python to Rust, Rust isn't going to be happy with the circular relationships between these types, and trying to implement `pets` or `owner` with references probably won't compile.
In cases like this, the most ideal, most idiomatic, go-to option is to break the cycle and try to achieve simple ownership. In this case, that would probably mean making the `pets` collection hold Dogs by value, and removing the `owner` field entirely. Any Dog methods that previously referenced `owner` would need a short-lived reference passed in as an extra argument now, or maybe we could change some of them into Person methods. If we can express our program in this style, that's almost certainly what we want to do.
But there are lots of programs where this doesn't work, at least not everywhere. Maybe a Dog can have multiple owners. Maybe a Dog can have no owner at all. Maybe Dogs want to track their relationships with other Dogs. If people and dogs are independent entities walking around in a game world, or if they represent rows from a couple of tables in some relational database, we probably have lots of problems like this. This is where we start reaching for patterns like Rc<RefCell>/Arc<Mutex> or indexes pointing into Vecs and HashMaps. (I think it's interesting that those Vecs and HashMaps look a lot like db tables.)
A point I want to emphasize here, though, is that even when the most important relationships in our program use these patterns, the majority of our object relationships are probably still simple. If each Person has a `name`, that's still an owned string. If they have an `age`, that's still a regular integer. When our program reads config values from the filesystem, all of our file handles and protocol data still follow simple ownership rules, use destructors for cleanup, and definitely don't get aliased anywhere.
In contrast, if we port our program to Java (or keep it in Python), we can't statically guarantee any simple ownership. Any time we pass a Person or a Dog or a HashMap or a byte buffer to some function, we might worry about that function retaining a reference to it, and we might start making defensive copies or reaching for immutable types. We can still use lots of simple ownership, and for most of our objects we probably do, but we've lost the benefit of a compiler that can check that for us.
Kind of a tangent: When we make aliasing mistakes -- which we can do in Rust in these fancy arrangements, or in Java/Python whenever our data is mutable -- that usually leads to "spooky action at a distance" bugs. Some operation on `foo` has mysterious side effects on `bar`, etc. We've all been there. But I think where these bugs graduate from "annoying" to "insanity-inducing", is when multiple threads get involved. That's when we really want the option of simple, statically-checked ownership, and that's where I think Rust-isms like "Mutex owns the data it protects" are a big improvement over the tools we had before.
Sure, a beginner may not know about these tools, but my point is about the manuality if fixing the problems, one almost doesn’t need any thinking .
- Much of the time, maybe even most of the time, memory corruption means security vulnerabilities. Logic bugs can be security bugs too, of course, but at least we can reason about them in local terms like "Is this particular part of the program security-sensitive?" Memory corruption doesn't admit the same kind of local reasoning. For any program that touches input from the internet, that's a huge deal.
- Just like C programmers can reach for Valgrind, Rust programmers can reach for design patterns that are more robust than Vec<T>. In this case, HashMap<u64, T> with an incrementing key is a very robust pattern. In practice (until you overflow that u64), that pattern catches 100% of your use-after-free-style mistakes. And I think a major advantage of Rust's approach here compared to Valgrind/ASan, is that it runs in production, so it works even if your test coverage isn't amazing.
- embedded programs on avr hardware, which is impressive that something like rust can compile down to an 8 bit micro. Doing some sort of crazy map/iterator filter closures and finding the resulting assembly being nice and tight makes me super happy.
- web servers and clients (a scraper that collects scores from a web page and serves its own page of aggregated scores to my family).
- a low level opengl tmux/terminal client (Rust's Enums were a killer feature here)
- a windows GUI program for installing a PC game mod (99% developed on a mac and cross-compiled)
- a cli app to "compile" svgs down to png sprites for a web based game
- a launcher for Emacs that uses native cocoa apis to display a dialog on error
Each of these cases ended up pushing different parts of Rust for me. I would say the killer feature that spans all of those is Cargo and the crates.io registry. There are a ton of very good libraries out there—it feels like the heyday of CPAN or RubyGems to me.
I also quite enjoy how many programming errors Rust finds at compile time. With Rust code I spend way more time fixing compiler errors, but when it compiles I typically don't find a ton of major logic errors. I personally find it extremely rewarding when I get something to compile and then it just works. This happens more in Rust than in other languages I've heavily used.
- It's easy to set up cross compilation.
- Cargo makes dependencies easy.
- Building is almost always just `cargo build` except for very exceptional projects, same with `cargo test`.
- `cargo fmt` makes formatting easy and built in.
- Clippy gives actually useful lints and can automate most refactorings.
- Rust-analyzer is fast to index decently large projects, works with almost all language features (even complex ones like proc macros), and has some real time-saver automation built in.
Working with the borrow checker can feel like a pain, but in many cases, feels like a fun puzzle (writing hobby code, I'm not under time pressure, so it's 'solve a puzzle' and not 'oh fuck deadline coming up').
But really, the tooling of the language is where it shines.
Phenomenal performance, drop-in concurrency support (`iter()` -> `par_iter()` with Rayon), and a great ML-based type system that makes writing correct code easy.
It also has some of the best developer UX/tooling out of any language I've used.
1. Much more expressive type system: no more `interface{}`.
2. Algebraic data type with `enum`. Exhaustive check.
3. `serde` crate is lightyears ahead of Go's `json:"wtf"`.
4. Compile-time checks against DB with `sqlx` crate.
5. Logging is a breeze with `tracing::instrument`.
6. Using macros to reduce boilerplate has better UX than Go codegen.
What these give me is more confidence in development/refactoring because the compiler can guide me most of the way.
Low lights:
1. Build time.
2. Async is still clunky. e.g. Try to implement an `actix-web` extractor or middleware.
3. Please just stablize nightly rustfmt features already.
Would you mind sharing some of your experiences? Do you find the 'stubbornness' of the compiler frustrating at all? How do you find the tooling?
Also, I've considered Diesel for ORM, so was wondering if you've been using that too.
I actually love it. The more work I can offload to compiler the better. One simple example that frustrated me in Go was adding a field to a struct. You add the field the the whole thing still compiles even though the zero-initialized value probably broke your app logic. In Rust if I add a field to a struct, the compiler warns me about all the places that I need to double check.
> Would you mind sharing some of your experiences?
I highly recommend zero2prod book which is well-written, practical, but still teaches the essential principles (https://www.zero2prod.com/). You basically deploy a CRUD app to DigitalOcean from scratch. The best way to ramp up IMHO.
> How do you find the tooling?
Cargo is sweet. rust-analyzer is all I need. I need less extraneous tooling to be effective. For example, in Go I might use a task runner to watch the repo and run tests when I change a file. But in Rust I can just follow the rust-analyzer highlights and manually compile less-frequently.
> Also, I've considered Diesel for ORM, so was wondering if you've been using that too.
I was not happy with GORM (https://gorm.io/index.html) and never had a satisfactory experience with any ORM. I'm a fan of writing plain SQL, even in Go. It's just that with Rust sqlx I can get compile-time checks against the schema. It's not anything new (see Haskell), but it tightens the feedback loop and I have full control of the performance.
> You add the field the the whole thing still compiles even though the zero-initialized value probably broke your app logic.
Ah, I found Python notorious for the same reason.
I've found ASP.Net's ORM to be quite good, though this is only with a year's experience, so perhaps I'm missing some cracks that might emerge later.
It would have been impossible for me to build this in any other language. I knew absolutely 0 about developing for Windows when I started this, but I quickly realized that the Win32 API is a very old beast with a lot of footguns that Rust saves me from on a daily basis.
On top of that, with parking_lot I can quickly and easily detect incredibly rare instances of deadlocks, and in general have to worry very little about concurrency. All of this allows me to stop worrying about the noise and focus on the real work of how the window manager should behave.
1. Based on Github Stars
Main reason ? It works. The compiler has real error messages that are helpful making the onboarding of anyone having to fix problems a far far better situation than any of the competitors. It has a proper modern toolchain.
All of these would be killer features. All together ? No competition.
I also write cli tool and all kind of parsers for embedded language. The libraries here, in particular if you want good error messages and UX, are simply the best in all other environment i know of.
The mappings in https://github.com/kylebarron/geopolars/blob/master/py-geopo... for example look very easy to follow.
I considered Node.js, C#.
I went with Rust for a few reasons:
- Easy to build small portable binaries (as a primary language feature).
- The type checker ensures type consistency when writing out to SQL tables (SQLite is loosely typed). Code that reads from the SQLite database implicitly benefits from Rusts strong type checks.
- Macros to convert structs to SQL insert/updates.
- Reduce the chance of errors at runtime.
- Leverage as much as SQLite's write throughput as possible.
- When converting Stripes Open API JSON spec into Rust code (using another Node.js program), the Rust type checker ensures I have a well formed HTTP client - the strict compiler makes it a good target for generated programs. Read more about this idea at (https://willcrichton.net/notes/rust-the-new-llvm/).
FWIW in recent SQLite you can turn that off using “STRICT” tables, however that means the available types are restricted to INT(EGER), REAL, TEXT, BLOB, or ANY.
ANY is also “Stricter” in strict tables: it does no conversion whatsoever, whereas in normal mode it’ll try to parse literals to numbers, and store a number in that case.
It was hard to get going, and I still only know basics, but everything just works! Typing, borrow checker, the matched results, all of that makes code bullet proof.
The traits/generics/macro system made that feasible, though messy. You can do similar things with templates in C++
It's also quite niche, so I'm not sure if it's for everyone's use cases.
Also, lately working on some identity/p2p stuff.
Best parts IMO (not in any order and not exhaustive):
- Not having to consider exceptions (panics are still there, but are much closer to the "exceptional" behavior exceptions are/were meant to convey)
- Explicit error handling with (usually) good error types/representation (instead of the nebulous "`err` that is a string" I've seen in what little Golang I've done)
- Threading without worrying about race conditions
- Dependency management
- Data structure focused (instead of object focused)
- The built-in `#[test]` features are also great and make it easy to test internal code without exposing it in some strange way purely for testing purposes.
FWIW, I find that it also makes your C++ far better because now you can more easily "see" lifetime problems that people tend to write out of habit (`return stackstr.c_str();` is a…favorite that compilers now warn about).
Compared to Javascript, Rust safety features allow me to iterate quickly and improves my productivity from "I won't be able to complete this project" to "it's trivial, just some work". Compared to Typescript, Rust gives me some performance and types that actually represent the data I use, but I am not sure if I lost a bit of productivity (the TS tooling is horrible, so programing a bit slower isn't the entire story).
I don't consider this a problem Rust is good for, but one that all the more suitable languages still don't support.
Java, .NET languages and C++ are what pay the bills for me.