Rust 1.63
blog.rust-lang.org
blog.rust-lang.org
Now we just need to do it all over again once Polonius finally arrives. :P
Just to be clear, these are breaking changes. If the new borrow checker didn’t break existing code, they would have removed the old one long ago. They’ve just decided that the observable effects of the breaking change in the ecosystem are small enough (especially after warning about them for years) that they are okay making the breaking change without a semver-incrementing Rust 2.0.
To be fair, the reason for the breaking changes is generally to fix soundness issues (of course, not every example of code now banned will exhibit unsoundness), but that doesn’t cover all of them. There were some completely sound (albeit useless) examples of partial initialization that broke with NLL:
https://github.com/rust-lang/rust/issues/21232#issuecomment-...
https://github.com/rust-lang/rust/issues/54987
Strict adherence to semver would require still accepting this code.
If this is so, how are they maintaining edition compatibility? It seems like you could write code for the 2015 edition that would build with the new compiler, but not an older (2015 edition compliant) compiler.
The edition compatibility story here is interesting. When the 2018 edition came out, the new borrow checker was only turned on for code on the 2018 edition. Meanwhile, in the 2015 edition, it would run both borrow checkers and yield a warning if your code passed the old borrow checker but not the new one. After a migration period, the new borrow checker was eventually made the default on the 2015 edition as well. Because of the aforementioned soundness bugs fixed by the new borrow checker, this was seen as acceptable (soundness fixes are explicitly allowed by the stability policy, although the Rust developers still try to provide migration periods to make them less painful).
I like that previous editions get new features when possible, unlike say C++, but I wish they'd gone with semantic versioning for the language version, like python did when both 2 and 3 were active. Oh well, I just need to remember that the edition is like the major version, and compiler version is sort of like minor version, and both are needed to specify forwards compatibility.
2.17
1.17
What would you call the compiler binary that could compile both language v1 and language v2, with (shared) minor-version .17?The issue is that semantic versions are a hierarchy, whereas Rust edition x compiler version are a matrix
They do not promise new code works with old compilers
I ask because I remember when trying to learn Kotlin, the docs made a big deal of structured concurrency[0], and golang of course is known for its concurrency story.
[0] https://kotlinlang.org/docs/coroutines-basics.html#structure...
For Rust it allows thread to play better with lifetimes, which means there are scenarios where you don’t need the overhead of a lock (and usually the Arc that does with it, unless you go with a global) or queue, you can just work with on-stack “unsynchronised” borrows and it’s thread safe.
That means lower syntactic and runtime overhead in the cases where it’s an option.
It was already available in crossbeam so it’s not novel novel, but not needing a dependency for it is useful still.
In short, this means that thread pools and async runtimes cannot use this feature to provide borrowing across tasks. We're still stuck with Arc-ing everything that crosses task boundaries.
Prior to 1.0, there were "true" scoped threads (with a lifetime 'a on the join handle), which let you borrow from the parent thread. (This could have extended to tasks as well, had they been around.) unfortunately it wasn't safe together with the much more popular Arc/Rc, which wasn't 'static restricted, despite it's ability to cause cycles and leak. So those powerful scoped threads were removed and "destructors are not guaranteed to run in safe Rust" was formalized instead.
Seeing how popular async became, I wonder if this was the only and right way to move forward. I'm not against Arcs, they have their place. But if you can use normal RAII it's much preferable.
They can't use this function, but with some duplicated unsafe code they can still provide similar APIs for their callers, right? Things like this: https://github.com/jaboatman/tokio-scoped
That said, taking a step back it has crossed my mind that bringing back true scoping with unsafe in the impl might be an acceptable way forward, despite how upsetting this is to a lot of rustaceans. I think that the failure modes (ie triggering UB) for this case can be simply enumerated in a short list of "DON'Ts" (such as putting a scoped join handle in an Arc).
OTOH, since leaking was formalized and explicitly allowed (heck, even borderline encouraged through mem::forget), god knows if people have exploited that for other cases in true Hyrum's law fashion, that breaks the abovementioned idea. They certainly had the right to.
In the original acception I think you could literally do that, a scoped thread would just be a normal RAII object with various lifetime bounds.
With the new scoped threads, I’d think you need to create the scope in a and pass the scope handle to b so it can spawn the thread correctly.
Coding GUI/TUIs since MS-DOS and Amiga 500 days, and Web since 1996.
Speaking as a user, that's exactly what I don't like. I don't care about same UX on every platform; I'd rather have consistent UX across apps on the same platform.
As a user I prefer something that works, making sure one thing works consistently is easier than testing different native things that implement the same functionality. (Oh, and as a user I very much prefer that it does implement the same functionality.)
Native interfaces and behaviour are preferable over whatever options the app developer chooses to support.
In the case of electron apps, effectively running multiple browsers is also in the “what not to like” basket.
Native apps often have less input latency, and are significantly nicer to use.
> Allows faster iteration for small teams
As a user, I’d much, much prefer slower iteration if it meant the resulting app was better. Electron apps thrashing out pointless updates every other day just because they can isn’t always a positive in my book.
On desktop I don't run slack, discord, teams (and whatever wants to run separately) separately, if something that doesn't do meaningful local I/O and/or computation cannot run in a browser, then I don't use it if I can avoid it at all. And I hopefully can keep my streak of not developing nor shipping any forced electron based mess.
VSCode is fast. I'm surprised too. While native Windows things are just slow all over (with a beefy modern desktop).
Seriously, how the fuck can the start menu be this ridiculously slow? I have like 50 things installed, and I use about 5 of those. And it takes forever to find those as I start typing. (I have KDE flashbacks!)
Oh and super native Firefox is also a disaster when it comes to speed (I search for the same bookmarked pages in the awesomebar - the combined location and search bar, I have disabled the search part, because the UX was horrible, so it should just search in the local recency shorted LRU cached list of pages ... and it's dog slow and dumb).
Big billion dollar unicorns push those meaningless updates, not small teams (at least in my experience).
Everything. Make a website if that's what you want.
Mozilla tried "Firefox OS" and it's hard to do it. Chrome tried service workers and persistent sites/apps, but.. it's just lame compared to shipping a separate app that bundles a web view and a backend.
I've once picked ObjC for a whole program based on its native GUI, and spent way too much time chasing bugs caused by its thread-unsafety (Cocoa bindings are a trap in multi-threaded programs).
If you are doing web stuff, I'm sure Rust can be used effectively. But it is non-trivial and there are serious tradeoffs (learning curve, lack of ecosystem for web, hiring, etc). Teams would be better off sticking with python or JS for most web projects. Some perf-heavy backend or infra projects could possibly benefit from Rust, but otherwise it doesn't seem like the best choice to me.
It is definitely a cool language that seems to be gaining serious momentum. Could be fun to mess around with.
I understand why it exists for certain tasks, but I can just use a garbage collected language instead (preferably a modern and expressive one, like Kotlin) and save myself a lot of trouble, because in the end, the performance hit often doesn't matter.
1. The versatility of Rust means I can use it for truly "the full stack". E.g., in my current side project, I'm using rust to do audio decoding, resampling, running a speech recognition model, the backend of the app, and the frontend! Though I haven't yet started on all of those components, I expect to get some major simplification in my code because of this.
2. The "modern and expressive" languages all have fatal flaws, _for me_. Swift has a poor cross platform story/community, Kotlins tooling is poor if you don't use intelliJ, ocaml has a reputation of poor documentation. I understand that not all of these things will bother people as much as they bother me, but they do bother me a lot. Tbh, I'm still on the lookout for a lang that is actually statically typed(no TS or python), has an expressive type system, a good cross platform story, good, non proprietary tooling, and is at a slightly higher abstraction level than Rust.
As for 2, I'm very happy with Kotlin for the most part. I agree that there's no point in trying to use it without IntelliJ, but... IntelliJ is really good, and the community edition is free. Yes, the startup times can be slow and annoying, but what I get in return is IMHO worth it (even more so if you use Ultimate and frameworks like Spring). Also, command line tools for Kotlin are IMHO better than for Java, at least (e.g. ktlint). I agree that Swift outside of Apple software is a dead end.
Given your constraints, though, why are you ruling out TS (I've never used it, but I heard good things)? And what about Haskell?
Though, in all fairness, in your case you're probably comfortable enough with Rust's borrow checker that it doesn't slow you down that much anymore, so I understand why you'd continue using it. For someone who has never worked with it, it's a different story.
My (admittedly poor) excuse is that I really wanna keep using neovim (I really like the customizability) :D
> Why not TS/Haskell?
TS is amazingly expressive, and probably my choice after Rust for traditional full stack! But it's sometimes the case that the underlying dynamic nature of JS "leaks through" (especially when using third party libraries), and it does come with the rest of the JS baggage (undefined == null, no easy pattern matching, ..etc).
Haskell? I just realized that, from reading the interwebs, I've had an unconscious bias against Haskell as a mostly academic language unfit for industry use cases except in very narrow niches, but I def should check it out and make up my own mind.
> Though, in all fairness, in your case you're probably comfortable enough with Rust's borrow checker that it doesn't slow you down that much anymore.
True. On the rare occasion that it does, I just use the "cop out" of doing `.clone()`. I hope to get better though!
I used to be a big "vim for everything" person. Now, less so. I just use vim mode in my editor/IDE of choice, because what I'm really after is the superior text editing. Now, depending on the project and language, I'll use vim, vs code or IntelliJ. But I can see that if you're really into all the vim customisability, it's different...
Other than that, it's a great language with a great ecosystem.
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
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.
The traits/generics/macro system made that feasible, though messy. You can do similar things with templates in C++
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.
Also, lately working on some identity/p2p stuff.
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.
It's also quite niche, so I'm not sure if it's for everyone's use cases.
- 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?
The mappings in https://github.com/kylebarron/geopolars/blob/master/py-geopo... for example look very easy to follow.
- 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.
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
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.
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.
Java, .NET languages and C++ are what pay the bills for me.
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 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
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.
- 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 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.
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).
I'm realizing I don't know anything about compilers and very little about how languages work on a technical level -- stuff like "strong" vs. "weak" typing, memory management, stacks and heaps and garbage collection -- I've seen all those terms before but have no intuition for what they mean. I never have to think about these concepts because Python is such a high-level "it just works" scripting language.
Could anyone recommend some online resources for learning about the computer science of programming for someone with little prior experience? I'm not quite sure where to even start, but I'm eager to learn because at the moment I don't really feel like a "real programmer", more like a script kiddie. Heck, I only learned to use Git properly like 6 months ago!
I'd suggest doing the C parts and stopping at the python side. Once you stop, step into Rust and you'll see how much easier it is to perform all the things you were doing in C.
It's a dream once you understand it. You don't need to understand the math, just the general concept of type unification, and that type inference can flow in "both directions", as opposed to in e.g. C++ where `auto` inference only works in one direction.
Rust is not exactly HM type inference, but it's something close to it. http://smallcultfollowing.com/babysteps/blog/2014/07/09/an-e...
If you tried comparing it to something that wasn’t an array, or if you tried comparing it to multiple different-sized arrays, it would complain.
let array = core::array::from_fn(|i| i);
assert_eq!(array, [0, 1, 2, 3, 4, 5]);
The magic of type inference let array = core::array::from_fn(|i| i);
assert_eq!(array, [0, 1, 2, 3, 4]);
Is it because of the assert_eq? Can it use the length of the second argument to infer the length all the way back up in the from_fn call? assert_eq!(array, [0, 1, 2]);
If you tried both— assert_eq!(array, [0, 1, 2]);
assert_eq!(array, [0, 1, 2, 3, 4]);
—the assertions would never run because it wouldn’t even compile: error[E0277]: can't compare `[usize; 3]` with `[{integer}; 5]`People always complain about Rust's type system being verbose- well, this makes it less verbose. It knows what you need from the function call, so it doesn't make you say it. And as an added benefit, if the required type ever changes in a way that can still be generated by the provided code (eg. the function you're passing to needs a different length), you won't need to update any explicit type annotations at the call-site.
If that's ever undesirable - you want to be very precise about your code - well, then you can do that too by specifying the generic parameters explicitly.
let numbers = [0, 1, 2];
let doubled = numbers.map(|x| x * 2);
You wouldn't want to have to explicitly type out the length of the second array.> What's a sane/legit use case of such inferring in general?
Depends how you interpret "such inferring"
In other languages, this can be a brittle/expensive operation due to meh tooling. But with Rust, it tends to "just work".
Online viewers of source e.g. Github and such don't have all this information exposed yet, but they're incrementally getting there. For instance, "go to definition" works for Rust code in Github.
One could imagine a generic file format included as an artifact in source control, that online viewers could consume, to annotate spans of source code. Go to definition, show expanded type, etc.
let foobar = Default::default();
do_something_with(foobar); // where do_something_with takes a FooBar param
One I often use in unit tests: fn generate_ids<const N: usize>() -> [Id; N] { ... }
let [id_button, id_label, id_thing, id_other_thing] = generate_ids();
(before const generics were stabilized, the above code instead had multiple utility functions, eg generate_2_ids, generate_3_ids, etc) let mut x = HashMap::new();
x.insert(1, 2);
Note that nowhere did we need to specify the concrete type of the HashMap; the compiler sees that you eventually insert numbers into the map so it uses that information to fill in the generic type parameters for the key and value.I believe this is because of the signature:
pub fn from_fn<T, const N: usize, F>(cb: F) -> [T; N]
where
F: FnMut(usize) -> T,
While T is unconstrained, N is a usize, which is used in the closure, which we then re-use as the element. pub fn from_fn<T, const N: usize, F>(cb: F) -> [T; N]
where
F: FnMut(usize) -> T,
It takes a function with signature f(usize) -> T. So |i| i gets inferred to be a function taking a usize, and returning a usize (since we just give the argument straight back). So it infers [usize; 5] in the end.For example core::array::from_fn(|i| i) == [0i32] wouldn't even compile.
Which is in a way an even stronger showcase of Rust's type inference: the length of the variable is inferred from the constant array, but the type of the content of the constant array is inferred by the type of the content of the variable, which is inferred from the type of the closure.
The new implementation calls a closure and waits for the threads when that closure returns. Unlike the destructor here's no way to stop that code from running when it should.
const THING: Mutex<u64> = Mutex::new(4);
without using the once_cell crate. This is pretty cool! Also I realize that the Rust playground is still using 1.62.0https://play.rust-lang.org/?version=beta&mode=debug&edition=...
The playground rebuilds overnight (roughly after the nightly is released); releases don’t have as fixed timing so I tend to trigger a rebuild when I notice. This HN post was delivered before the git tag was published even, so I’ve started it.
Playground is updated now.
This might be what you want if you, for some reason, need to make different Mutexes with a 4 in them all the time. But, if you actually meant one Mutex, which intially has a 4 in it, then that's a static variable not a constant.
https://play.rust-lang.org/?version=beta&mode=debug&edition=...
Change FOUR to be static instead of const, and now when we print y each time it's gone up by ten because we're using the same Mutex which is what we presumably intended.
As the comment eludes to, each spawned thread is implicitly joined if you drop the join handle returned by s::spawn().
So in this case the spawned threads run serially.
From the docs on https://doc.rust-lang.org/stable/std/thread/struct.Scope.htm...,
> If the join handle is dropped, the spawned thread will implicitly joined at the end of the scope.
Plus the comment says
> // We can even mutably borrow `x` here, // because no other threads are using it.
Which doesn’t line up with another thread having a read-only borrow on it.
Anyway not at my machine to test right now, I could be wrong.
> If the join handle is dropped, the spawned thread will implicitly joined at the end of the scope.
"end of scope" here refers to the end of the closure, after the second thread is spawned.
You can trivially show that the threads exist in parallel by adding waits.
This gets pointed out by somebody in the comment section under every Rust update. The trope that Rust is getting harder to use is tired, and not really based in reality.
And while its fine for high level scripting languages like Python, because the whole idea is to abstract a lot of the functionality into the syntax, the issue with Rust is that its trying to be the next C and be applicable for writing critically important pieces of software like parts of the linux kernel. You absolutely must have a locked down language set for this. The reason C is still used for the kernel isn't because of circumstances, its specifically because there is very few ambiguities in the core language.
Which of the features in Rust 1.63 adds a divergent way of doing something you could already do?
> The reason C is still used for the kernel isn't because of circumstances, its specifically because there is very few ambiguities in the core language
What are some ambiguities in the core Rust language?
You don't need a "locked down" language to build an OS. In fact Linux just upgraded the version of C they use to C11. Time always marches forward, my friend.
You might have a point though in a few releases when GATs are stabilized. I'll keep an eye out for your username complaining about that release ;)
The first is a new library function that takes a closure as the parameter. This function has existed for as long as Rust has been 1.0 in the crossbeam library, it's just now also in std.
The second change is a new api type.
The third change is a library change to increase flexibility of that library.
The fourth is arguably a bugfix, where existing syntax could not be used in an edge case.
The fifth is unifying the compiler implementations so that some types of patterns now work in code using the oldest compatibility mode that previously only worked in more current modes.
I'm not sure it would be realistic to totally stop adding features to the standard library, or to stop making existing syntax work the way a Rust user would expect. I would hope that any actively maintained language does things like these.
For example, it seems as though "Word".contains(char::is_ascii_lowercase) should work, because after all "Word".contains(char::is_lowercase) works and char::is_ascii_lowercase exists and does what you expect, the same but for ASCII only.
However, for compatibility reasons char::is_lowercase takes a char, while char::is_ascii_lowercase takes a reference to a char, and so you can't just pass it to contains() and will need to write a closure to pass to contains to do ASCII instead.
Writing the closure is ugly, but it's nowhere close to how awful a compatibility break would be so even a million things like this (and so far maybe I can think of a dozen) aren't worth it.
but it's also true that there are quite a few threads that are conceptually simpler than GAT and are just left unfinished.
and there is a very serious push (or pushback with regards to GAT) to keep those promises.
https://github.com/rust-lang/rust/pull/96709#issuecomment-11...