The Plan for the Rust 2021 Edition
blog.rust-lang.org
blog.rust-lang.org
"The book" although a great book, is fairly dense with no exercises. Rustlings[1] help me apply alot of my learning and i supplemented the book with Tensors programming tutorials on youtube [2]. Recently i started developing a game in Bevy[3] and although its a bit over my head at this point i've managed to get a character moving and be able to kill a monster and receive a drop. My motivations for learning rust was to be familiar with substrate [4] as i'd like to switch careers into programming. Would also love any recommendations/tips & tricks by any experience rustaceans!
[1]https://github.com/rust-lang/rustlings [2]https://www.youtube.com/watch?v=EYqceb2AnkU&list=PLJbE2Yu2zu... [3]https://bevyengine.org/learn/book/getting-started/ [4]https://www.substrate.io/
Can you point me to some really good rust code on github? I've been reading through some and see how people do some interesting things but i don't have the mastery to understand which practices are better than others.
> f"" as a short-hand for a format string. For example, f"hello {name}" as a short-hand for the equivalent format_args!() invocation.
I'm excited by. I like the f-string formatting in Python quite a bit.
> Attributes are modeled on Attributes in ECMA-335, with the syntax coming from ECMA-334 (C#).
There's discussion in the corresponding Github issue of the RFCs repo (https://github.com/rust-lang/rfcs). If the folks who work in that area are supportive, the RFC will be merged. After that someone needs to implement the feature.
Take a look at the Readme in the RFCs repo for more details.
The array/iterator thing always baffled me when I first started learning Rust.
All these tiny DX improvements make the language more accessible and in turn a pleasure to work with!
> If the folks who work in that area are supportive
Rust has teams that make decisions to accept designs in their part of the project. They look at proposals and decide to accept, reject, or postpone them. This process can take a while, depending on all sorts of factors. Sometimes a design may be good, but it may not be the time yet, which is when postponing happens. Sometimes it's a "we don't plan on doing this" and that's when things get rejected. Even if a design is accepted, we don't require that people proposing the design do the implementation work, so if you do propose something and it does get accepted, it may take a while until it actually exists. Furthermore, stuff is in unstable at first, until people can gain experience with the feature, so even after an RFC is accepted, it takes some time until it's "stabilized," at which point it's part of the language proper.
Happy to answer any other specific questions!
Edit: For more context, the README says:
> If you are interested in working on the implementation for an "active" RFC, but cannot determine if someone else is already working on it, feel free to ask (e.g. by leaving a comment on the associated issue).
I went and checked out the issues and couldn't tell which ones were active. (I also don't plan on implementing an RFC, so no worries if this is something that should be apparent to someone more involved.)
I don't think there's a good way to filter on "active", it's mostly like, if you go to the issue and it's open, then it's 'active.' It's a bit odd, because in some sense, for the RFC repo, once the RFC is merged it's "done", and then rest of it is implementation and therefore tracked on the main Rust repo. The "C-tracking-issue" issues are ones that are open and tracking an RFC that's in implementation, https://github.com/rust-lang/rust/issues?q=is%3Aissue+is%3Ao...
("C" is short for "Category". These prefixes are... well the original ones made sense but I think there was some retconning going on at some point, haha)
In reality, f-strings work really well and are super elegant. I'm a convert now.
For a language that prioritizes safety, there are a surprising number of gotchas in Rust (another example is the large number of partial functions in the standard library).
(To be clear, what's happening with `array.into_iter()` is unsize coercion, not auto-dereferencing. `[T; N]` does not impl `Deref<Target = [T]>`. But the point is the same.)
struct S;
impl S {
fn foo(&self) { }
}
let s = S;
s.foo();
That compiles today. Your proposal would require it to be `(&s).foo()` a.foo(b,c);
vvvv
typeof(a)::foo (??? a,b,c);
vvvv
A::foo /* takes &A */ (/*so this is:*/& a,b,c);
vvvv
A::foo(&a,b,c);
Auto-derefencing is: a.foo(b,c)
vvvv // a is &X, so replace it with (*a)
(*a).foo(b,c) // wrong; should be a.foo(b,c); we called
vvvv // a method on the pointer, not what it points to
X::foo(???(*a),b,c) // should be A::foo(???a,b,c)
// continue as above
Auto-derefencing changes which type the method is looked up relative to, not just how the object is passed to it.I called it "auto-dereferencing" because that's what OP used when talking about `[].into_iter()`, and then clarified that it's actually an unsize coercion, not auto-deref.
Nope, it uses the same type (A), just blindly adds a operator to the argument based on which method is called. You could just as easily have `a.foo()` -> `A::foo(++a)` instead; it's just[0] that adding `++` would be largely useless.
0: You'd also need a explict annotation on the declaration of foo, but that's a convenience issue.
EDIT: hilariously exactly this happened three minutes after my post…
Same with the array change, the failure mode was breakage. While in this case, the method's names, arguments, and argument types are identical, the return type is different, which means other code expecting a certain type now has a different type. For that to silently change behavior, it would also need to match up on everything on the return type as well.
> In my opinion, the convenience of auto-dereferencing "as much as possible" to make method call syntax magically work through the indirection of references is not worth the lack of stability guarantees that it causes.
Auto-deref is just one way of many that the dot operator can resolve to an unintended method. This problem would still exist even if auto-deref were removed. What you're really objecting to is the existence of the dot operator at all. I think the dot operator pulls its weight, given how often it's used: I encourage you to check out Simon Peyton-Jones' thoughts on "the power of the dot" [1].
[1]: https://www.microsoft.com/en-us/research/wp-content/uploads/...
* the function's argument is a tuple (this is the more common way, not only in SML, but also how it's done most commonly in maths);
* currying.
I'm not a fan of currying, because it singles out the first argument over the others. However, the first approach (tuples) is very ergonomic, because now you can pass the return value of one function as an argument to a "multi-argument" function. One application of that is function composition. But another is a simple map. I've lost count of how many times in Rust I had an "o: Option<(A, B)>" and a "f: fn(A, B) -> C" and couldn't call "o.map(f)"; instead the programmer is forced to write more noisy code with lambdas. And lambdas sometimes don't play well with the borrow checker for mysterious reasons (even those that don't capture anything).
Also, I think it would be nice if the compiler generated named discriminants for every enum. Currently, "std::mem::discriminant" returns an opaque "std::mem::Discriminant<T>" (which doesn't have a name). In order to compare discriminants I need to create a full-blown enum value and extract the discriminant from it later. I've worked around this problem by using the "strum" crate, which has a macro to produce another (plain) enum, but having the discriminant type be named "std::mem::Discriminant<T>" (to signal the connection to the original enum) and have named variants at the same time would be the best of both worlds.
EDIT: If I knew more about possible breakage caused by my first suggestion, I could probably start working on an RFC. However, I feel I don't have enough knowledge about Rust to anticipate that.
For my second suggestion I think I'm going to try to propose that. I'm reading the docs on the process now.
That's great news.
> that could solve your issue if `mem::disciminant<Enum::Variant>` returned what you wanted.
I think you misunderstood, or I explained it poorly.
Given an:
enum Enum {
A(int),
B,
}
I would like mem::Discriminant<Enum> be the same type as if there was such a definition in code (illegal in Rust, because Rust doesn't have specializations): enum mem::Discriminant<Enum> {
A,
B,
}Non-serious solution:
#![feature(fn_traits, unboxed_closures)]
pub fn foo<Args, C>(o: Option<Args>, f: impl FnOnce<Args, Output = C>) -> Option<C> {
o.map(|v| f.call_once(v))
}
https://doc.rust-lang.org/stable/std/ops/trait.FnOnce.html"Non-serious" because it won't be stable any time soon, and it's not as concise as `o.map(f)` anyway.
More seriously, I don't think `o.map(f)` will ever happen, because it'll be backward-incompatible in the case of `o: Option<(T,)>` and `f: <U>impl FnOnce(U)` - `U` used to be `(T,)` and now would be `T`. Something like `o.map(spread(f))` might work if vararg generics are added to allow a generic `spread` that works for any tuple and matching-arity FnOnce.
This "foo" still suffers from the same problem as "Option::map" does: it is hard-coded for functions of fixed arity (2 in this case, 1 in Option::map's case).
Needing to write this function doesn't shorten the code. The SML-inspired solution allows one to write an "Option::map" which accepts functions of all possible arities (since they are all 1).
But yes, if there are a lot of places where I want to call "Option::map" for an "f" which takes 2 arguments, that could work. Although, personally, I think I'd still opt for an explicit lambda, in order to communicate intent more directly (avoid one level of indirection).
Hence the last paragraph in my comment.
o.expect("Error")?.map(f)
if you just need to convert a Option to a value in-line and None is an error. o.ok_or_else(|| anyhow("Error"))?.map(f)
I've been writing too many of those forms lately, decoding a tree data structure of known form which might be wrong, but usually isn't.The Ceylon programming language worked that way[1] and it was really cool to be able just what you wanted: `o.map(f)`.
It's a shame that other languages didn't seem to pick up this idea... I've no idea if this could be made to work in Rust though (given Rust has no GC and it would go against its philosophy to allocate parameters like this on the heap just to create tuples - maybe the compiler could get rid of the actual tuples altogether?).
[1] https://ceylon-lang.org/blog/2012/12/21/tuples-and-functions...
Funnily enough, internally this is how closures parameters are represented internally, as a single argument that is a tuple of all its parameters.
pub trait Fn<Args> {
type Output;
extern "rust-call" fn call(self, args: Args) -> Self::Output;
}
where Args is a tuple of all the actual arguments.You used to be able to cause an ICE on nightly if you used something other than the correct argument[1], but instead it now tells you that it should have been a tuple:
error: functions with the "rust-call" ABI must take a single non-self argument that is a tuple
--> src/main.rs:7:5
|
7 | extern "rust-call" fn call_once(self, args: i32) -> Self::Output {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0059]: cannot use call notation; the first type parameter for the function trait is neither a tuple nor unit
--> src/main.rs:12:5
|
12 | x(42);
| ^^^^^
[1]: https://play.rust-lang.org/?version=nightly&mode=debug&editi...I stand corrected.
BTW, for Rust beginners who might be concerned that o.map(f) is somehow a difficult problem, the solution in Rust is simple: o.map(|(a, b)| f(a, b)).
I don't understand, is this a copy/paste mistake?
What I mean is, with the "strum" crate I can use a proc macro to automatically generate an "EnumDiscriminant" enum for an "Enum" enum, where "Enum" is defined as:
enum Enum {
A(u32),
B,
}
and "EnumDiscriminant" is defined as: enum EnumDiscriminant {
A,
B,
}
However, the only thing linking those two is a naming convention. I would like to have a universal name for such discriminants.The Rust standard library already has such a discriminant type for any enum - mem::Discriminant<T>, where T is the enum type. However, in order to get the discriminant of Enum::A(5) I need to write mem::discriminant(Enum::A(5)), which introduces noise (especially with bigger tuples or structs) - this 5 here is irrelevant. The mem::discriminant() function is useful for contexts where I have a variable of type Enum and want to get its discriminant, but when I just want to pick a particular discriminant, it's too noisy. I would like to be able to name the discriminant directly. I.e. I would like the standard library type Discriminant<T> be defined for each enum the same way EnumDiscriminant above is defined for Enum. So I could just write Discriminant::<Enum>::A.
What a refreshing take! One of the things I have admired about Rust is the effort from the start to build a healthy, welcoming, non-toxic community, and to prioritize personal well-being.
Plus of course it gives people on the project more confidence in saying "I have to have my evenings to recharge, I can't crunch this feature to hit the deadline". Which is what we should all be able to say.
I'm not sure if the changes discussed there are going to be rolled in or not. It's certainly an interesting thread.
Holy shit yes. Thank you thank you thank you. This fixes my biggest annoyance so far!
I like this.
In my experience it was cool. I was impressed with the speed. I tried to make a cryptocurrency (non-distributed) and that was cool.
I don’t think in low level yet, but I’m trying to figure it out.
I'm not sold on the raw identifier syntax and we'll see if the changes are actually as good as they claim, but in general this seems like a great way of doing language 'versions'.
Rust is definitely learning from the past and I'm really interested see what we learn from how they fuck up too
Yeah don't worry we'll fuck up for sure, haha. One could argue we've made a few of those already...
Also, most of these changes enable new things, and are only in the edition because of some low chance of breaking existing code. The 2018 edition had some major changes, so 2018 code looks pretty different - the ? operator, big changes to module use, impl/dyn Trait, and more. I suspect that most code bases you won't be able to tell whether it's 2018 or 2021 without looking more closely.
Eh, why not. We nerds are used to retconning in all the sci-fi universes anyway.
Another example how editions aren't much different than mixing language versions on other eco-systems, just another way of achieving similar results.
<!--
If you really can't wait, many features are already available on
Rust [Nightly](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html)
with `-Zunstable-options --edition=2021`.
-->I am looking forward to master Rust in the coming 4-5 years.
My personal recommendation to anyone just writing code as a learning exercise is that they start with using Rc<> wherever a borrow checker-related issue arises. Then you can get used to the rest of Rust. Once you feel a lot more proficient you can go back and start replacing Rc<> with references (and you might find that you don't even need to in many instances anyway)
But for the case where the data is modified, Rc often goes with RefCell as Rc<RefCell<X>> and allows in-place mutation called interior mutability. This means that the other code that got the copy of the Rc will observe the write effects.
(I’m serious about this, as a user for eight years and casual trainer for several. The fact of the matter is that when the borrow checker complains, it’s right to complain, >99.99% of the time, even if you had thought carefully about it and were sure you got it right and that the compiler’s complaint was unnecessary.)
I suspect this is because you naturally get to an instinctive form of ownership model similar to Rust. Since it is a really good way to reduce complexity. Rust then, merely formalizes and name it for you.
I feel like this verges dangerously close to patting ourselves on the back, but...
>I suspect this is because you naturally get to an instinctive form of ownership model similar to Rust.
I do feel this is more or less correct. I've only had a literal handful of "battles" with the borrow checker (over a couple months spent with Rust); the rest seemed to naturally parallel the ownership semantics I wanted in my code anyway. Ownership meaning everything from "where does a value arise" to "what parts of the code have access to that value in the first place". It forced - or rather nudged - me to think about my architecture in a way that led to a rather clear design, although it might have taken longer than in another language. In another words, I did move slower, but refactoring is much easier and less frequently needed, and that's a tradeoff I'm all for.
Compared to what? I have written about 10,000 lines of Rust so far... unfortunately, I stopped recently because I had a few huge refactors I had to make on one of my favourite projects which almost cost my sanity. The reason is that, in Rust, when you want to change the lifetime of a certain struct for example, you need to make sure that everywhere that's used it's only used in the new right lifetime... that turned out to be incredibly difficult to do and required heavy re-writing of many usages I had. Another huge refactor I had to struggle through was changing how I managed errors, and creating a new structure for my errors to be able to capture more information. Again, ran into huge issues which in a language with GC wouldn't exist. I still intend to go back to Rust as I do enjoy it even after all these issues I've had, but to claim Rust is easy to refactor strikes me as completely the opposite of the truth.
What?
> As a solution, Rust 2021 will use a new prelude.
That is, we cannot add it into the existing prelude. But we could have a new prelude, tied to the new edition.