Announcing Rust 1.28
blog.rust-lang.org
blog.rust-lang.org
* Partial borrow. I can't borrow a field of a structure and then reference another field. I can't cite how many times in a few weeks I've been forced to rethink and split my structures just to work around this, making my code harder to read and write for no good reason than working around this limit of the borrow checker.
* The Rc/RefCell/Box dance is very verbose, when you just want to share a reference to some long-living object within other long-living objects. Worse than verbose, it also causes a runtime overhead because of the RefCell.
* No simple way to declare non-const global objects, like something as easy as a global HashMap of constants strings to constant integers. I'm a little spoiled from Go's excellent support of init functions (both compiler-generated and manually written), but this is something I really miss in Rust. lazy_static! is a workaround which is harder to use and forces the code to be thread-safe even if no threading support is necessary (that is, I can't use my non-thread-safe classes in a lazy_static).
* In generic code, there is no simple way to represent integer types. You need to use the "num" trait which breaks the "as" operator causing big pain and less readable code (as it's normal to covert between integer types).
* No integer generic parameters. We have to use the "typenum" crate which ends up causing less readable code and generates C++-like error messages.
* PhantomData is awkward, it absolutely sounds like something the compiler should handle, not the programmer.
* "Wrapping<u32> + 1" doesn't compile, but is there a good reason why it does not? I really wish Rust added wuNN/wiNN native wrapping types, or evolved in a way that I can write the same code with "u32" and "Wrapping<u32>" (another example: "as" doesn't work). Right now, it seems like the solution is half-baked: it tries to use the generic to avoid adding a native type, but the code that users must write is more bloated compared to native types.
It would be difficult to implement something better, yes, but I'm also not sure it'd be better. In OO thinking, the question of which fields a method accesses should be an implementation detail, which shouldn't affect how the code calling it can look.
If the borrow checker was smarter, then a change to a class implementation could break the code using it.
Also, auto-`Deref` can make this even more surprising! If you have some `foo: Rc<Foo>`, then `&foo.bar` borrows the whole struct via `Rc::deref(&self)`. You can sometimes get around this by taking a full dereferenced borrow yourself -- `let foo: &Foo = &*foo;` -- and then work with partial borrows from that. Same goes for `DerefMut`, or really more so since `&mut` is exclusive.
impl TR for S {
delegate * to self.f;
}
then calling TR methods should only require borrowing/owning f rather than the whole of self.* Partial borrow.
The status of this is unclear. It does work in many cases. It's complicated. I would push back a little on the "no good reason" though, but this is gonna be long so I'll leave it at that.
* Rc/RefCell/Box
Yes, this is painful, but that's because shared ownership is painful. Rust is pushing you away from a certain kind of architecture. Many people think this is a good thing...
* No simple way to declare non-const global objects
That is lazy_static. But
> forces the code to be thread-safe even if no threading support is necessary (that is, I can't use my non-thread-safe classes in a lazy_static).
This is perceived as a good thing; if you code changes later to use threads, you're not hosed.
If you want something that's not for multi-threading, then don't use something that can be used with multiple threading! TLS is probably what you actually wanted here.
* In generic code, there is no simple way to represent integer types.
We went through three versions of these traits, and were not happy with any of them. It's a tough problem.
* No integer generic parameters
This is coming; the design work has been accepted, and now it's on to implementation. It's expected in nightly by the end of the year and probably stable early next year.
* PhantomData is awkward, it absolutely sounds like something the compiler should handle, not the programmer.
The compiler could try to infer variance, but that means that if your code changed, you'd have a breaking change. In general, around interface boundaries, Rust requires you to state what you want.
It's even less awkward than what came before it, for what it's worth.
* "Wrapping<u32> + 1" doesn't compile
Rust is pickier about numbers than many langauges; 1u32 + 1u8 doesn't compile either. Many people find this valuable; you don't have to memorize complex promotion rules, and you always get the semantic you asked for.
> Rust is pickier about numbers than many langauges; 1u32 + 1u8 doesn't compile either. Many people find this valuable; you don't have to memorize complex promotion rules, and you always get the semantic you asked for.
Although I'm happy with rust avoiding automatic promotion across the board, Wrapping<u32> + {integer}` could be handled more gracefully, in the same way that `u32 + {integer}` is handled.
Not all partial borrows are safe, but a lot of them are. Most programmers know how to write safe code coming from different languages without borrow checker; they do put bugs sometimes -- that's true -- but most of the times the code I write is safe. It's just that there's no way to tell Rust that it is, and the workaround has a global impact on the code being affected (it forces to split structures, which greatly increases the cognitive load in reading and writing the code).
> * Rc/RefCell/Box > Yes, this is painful, but that's because shared ownership is painful.
But the point is that I do NOT want shared ownership. What I want is to have a single owner of a long-living object, and several long-living references to it in other objects. When I say "long-living", I mean that the liveness of those objects is not visible through the call stack, so I can't use borrowing references. The only way to model this today is with shared ownership, which implies more than I would be willing to imply.
The same problem is apparent when trying to implement the callback pattern (which I forgot to list in my OP). Callbacks are basically impossible to do in Rust, and I found out I must have used callbacks everywhere in my last many years of programming in different languages, because I feel helpless in Rust when structuring code without being able to register and use callbacks.
> This is perceived as a good thing; if you code changes later to use threads, you're not hosed.
This does not sound very Rustecean :) If Rust is not opinionated and gives me full power with zero cost overhead, why does it force me to pay for thread-safeness (overhead) so that I can use threads later (opinion)?
Notice also that code which is not thread-safe doesn't prevent me from using parallelism. It just prevents me from moving objects across threads. But I can surely leverage the power of multi-core with threads, as long as the thread-unsafe objects are created and used within a single thread. So I'm not sure why I should make them thread-safe... just because I want to use them in a non-const global.
> If you want something that's not for multi-threading, then don't use something that can be used with multiple threading! TLS is probably what you actually wanted here.
What I want is actually much simpler: let me initial globals with runtime code that the compiler schedules for me to run before main. C++ does that very well; Go does that optimally. Either approach is good enough.
PS: TLS is potentially overhead again: indirect reference on non-PIE binaries.
> Rust is pickier about numbers than many langauges; 1u32 + 1u8 doesn't compile either.
I love that "1u32 + 1u8" doesn't compile, I hate integer promotion. What I'm saying is that "1u32 + {integer}" compiles, while "Wrapping<u32> + {integer}" doesn't. I think it's been a bad tradeoff to force people to use generics for wrapping integers when generics are still not good enough to implement a type which behaves like a native integer on common operations like "+ {integer}" or "as <type>".
Yeah, to me it sounds like you exactly want shared ownership. What you're describing is what shared ownership is.
> Callbacks are basically impossible to do in Rust,
I wouldn't go that far; yes, sometimes you need to do that dance for them, but not super often. It feels like maybe we've had wildly divergent experiences here :)
> why does it force me to pay for thread-safeness (overhead) so that I can use threads later (opinion)?
Because you're using a feature that can be accessed through multiple threads, even if you're not doing it now. If you use the version that can't, then Rust doesn't force you into it.
> let me initial globals with runtime code
This, in many cases, will be solved with `const fn`. But that runs at compile time, not before main. Life-before main has a ton of issues and is a huge pain.
Rc/RefCell/Box are verbose because they deal with shared ownership, and Rust forces you to solve problems around shared ownership that other languages don't. This is usually a good thing, but in times where you do require this type of architecture, there's a verbosity cost. Arguably the real tradeoff here is a loss of brevity for a gain in correctness.
Non-const global objects has definitely been a pain point for me. `lazy_static!` has always felt like a hack (although a community member released a possibly-better alternative just today[1]). I personally think HashMap literals might make much of the pain here go away.
`Wrapping<u32> + u32` is a result of Rust avoiding doing implicit coercion, which has advantages but also comes at the cost you've enumerated here.
[1]: https://www.reddit.com/r/rust/comments/9406rl/once_cell_a_la...
Would there be a space for a mode inside Rust, call it a DSL if you will, where Rust behaves like those other languages? I mean that in this mode it would use the heap by default and copying to not alert the borrow-checker?
It'd also be a worry if libraries started doing stuff like this; it'd be harder to use from "normal" Rust.
Yeah, that's the annoying part of Rust :(
> I can't use my non-thread-safe classes in a lazy_static
Well, you can, if you wrap them in Fragile/Sticky from https://github.com/mitsuhiko/rust-fragile :)
> PhantomData is awkward, it absolutely sounds like something the compiler should handle, not the programmer
This is quite common in Haskell (called Proxy there), for different reasons (not for lifetimes since there are none — but for passing type-level stuff around calls), but because of that, it feels completely natural to me.
Messages like that make coding low-level stuff fun again.
- Global allocators
- Improved error message for formatting: provides a specific reason why it is invalid
- Library stabilizations: NonZero number types
- Changes to Cargo: src directory can not be modified
This is not so much for optimization but for type safety (i.e. avoiding bugs).
How viable would it be to implement an optimization step which seeks to minimize the number of these checks?
EDIT: Steve answers a semi-related question in [1]
[1] https://www.reddit.com/r/rust/comments/8u0ggw/trpl_is_releas...
In theory, in the future, when Rust supports constants as template arguments, one might even add a const argument, the value that should be used as "null". E.g. if my u8 never reaches 255, I could use something like Never<u8, 255>.
The RFC covers this. https://github.com/rust-lang/rfcs/blob/master/text/2307-conc... Also see the RFC discussion at https://github.com/rust-lang/rfcs/pull/2307
Basically the generic NonZero already existed, but it's hard to express safely when used with arbitrary user types.
>In theory, in the future, when Rust supports constants as template arguments, one might even add a const argument, the value that should be used as "null". E.g. if my u8 never reaches 255, I could use something like Never<u8, 255>.
This requires const generics, so it could happen when they're implemented. (This is also mentioned in the RFC discussion.)
struct RectangularArray<T, const WIDTH: usize, const HEIGHT: usize> {
array: [[T; WIDTH]; HEIGHT],
}
const X: usize = 7;
fn main() {
let x: RectangularArray<i32, 2, 4>;
let y: RectangularArray<i32, X, {2 * 2}>;
}
I haven't read enough about singleton types to know the exact details about their similarities, so I'm not sure.Fully general dependent type would allow passing in runtime values, which is significantly harder to type-check and execute
The real motivation is
> With NonNull covering pointers, the remaining use cases for NonZero are integers.
It's unclear how extensible we truly want this to be, and so we decided to go with the concrete types. In the worst case, we'd eventually re-introduce some sort of NonZero<T>, and deprecate these types. This was deemed worth it to let people use this important feature on stable, rather than waiting and seeing if we ever decide to do otherwise.
enum Option<T> {
Some(T),
None,
}
Under the hood, this is a "tagged union"; Rust has to have enough space for the T, but also for a tag, to say which variant is the current one.If T is a NonZero type, then we know that zero is never a valid value. That means that instead, Rust can use the zero value to mean the None case, and any other value to be the Some case, completely eliminating the extra tag, and reducing the size of the Option.
This is a good example of a "zero-cost abstraction"; an Option in this case has absolutely no extra associated overhead at runtime.
Note that these optimizations aren't specialized for Option; they apply to anything that has this kind of shape.
I suppose you could always wrap NonZero with an accessor that adds 1 if negative to achieve the same result, though that might be too much overhead to be worth it.
As for the rationale behind moving away from jemalloc as a default, the tracking issue is here: https://github.com/rust-lang/rust/issues/36963 (TL;DR: lower maintenance burden, easier cross-compilation, smaller binaries, better system integration (including packaging), and the ability to use Valgrind).
https://doc.rust-lang.org/std/boxed/struct.Box.html#method.i...
After calling this function, the caller is responsible for the memory
previously managed by the Box. In particular, the caller should properly
destroy T and release the memory. The proper way to do so is to convert
the NonNull<T> pointer into a raw pointer and back into a Box with the
Box::from_raw function.
I would guess that this might become a fun latent footgun in crates.io code -- everyone writes their unsafe code in a way that Just Works under the default allocator, but then things break down in confusing ways when enabling alternative allocators, which should be safe to do even when using external crates.The opportunity cost of figuring out which one is better can often be higher than just trying out both choices. [1]
Someone who asks whether they should learn C++ or Rust probably doesn't know this – otherwise, they wouldn't ask.
https://www.reddit.com/r/rust/comments/71w6ht/is_there_a_c_f...
C++ sucks big time when it comes to "management" - there's no official compiler, no official package manager, no package registry, no easy dependency handling. It's easy to start with sane C++ and then "oh, I need a library for X", and you just wandered into a forsaken of hell accidentally. [ https://i.imgur.com/a4CVG.jpg ]
In the past you had to learn a lot of languages and platforms and you still need it in some cases. But for my particular needs, i've managed to narrow down mostly to 2 languages: C++ and Swift. But thats for me, for others it will be different things.
But dont try to find the perfect language.. dont be a hostage to any tech suffering with stockholm syndrome with a given piece of tech.
This weird trend of one tech-fits-all started back in the nineties with Java, with things comming out straight from the religions like 'evangelism', and calling people infidels for trying different things.
Maybe whats work for me will work for you too. I bet you can narrow down to 2 langs to do almost anything you want.
About C++ vs. Rust; Fear not. Modern C++ can basically 'anotate lifetime' in methods contracts by using smart pointers and move when you need. I prefer the C++ approach, have no problems with leaking, lifetime or whatsoever and deal with very large codebases programmed by large teams.
But of course, if i were to create a program to control a nuclear reactor or a submarine i would consider using Rust or Ada. But particularly i find Rust programming more stressful than C++ without (almost) nothing to gain. And on the language design aspect where Rust is superior to C++, i prefer to use Swift if i can, which i consider 'a better Rust'.
By the way in a lot of cases you might consider using Rust or C++, you can use the joyful Swift. Using a powerful language with less of a burden.
TLDR; Where some people will try to use Rust for everything, i prefer to use a Swift/C++ combo, and given you can mix both without much problem in a single program, i hardly need or miss anything else.
What cases are you thinking of here?
Is there a book (or other type of resource) that approaches C++ from the "modern" direction, i.e. with a focus on correctness?
The lack of generics in Go is a sore spot. It's quite possible to solve. The Go designers are not solving it, because they want other things more than they want generics, and they aren't willing to give up those other things in order to get generics.
For any given project, that could still be the wrong choice. For that project, then, reject Go. But don't totally reject the language because you don't trust the Go leaders' judgment on language design. It is more reasonable to mistrust your own.
Check this out https://www.youtube.com/watch?v=sX8r6zATHGU