[Roadmap 2017] Productivity: learning curve and expressiveness

Structs where #[derive(new)] would apply should have all their fields public in the first place.

Usually you write constructors, getters and setters instead of using public fields in order to be able to change the internal representation of a struct without modifying its API. That’s not possible if you use #[derive(new)] as you’d break that API.

Plus if you can write a dummy constructor for your struct, that means that there’s no possible invalid state for the values of the fields of the struct (if there was one, you’d need to check in the constructor, which you can’t with derive(new)). Therefore there’s no need to have private fields for that struct.

4 Likes

C++'s Concepts TS has Concepts like Regular, SemiRegular, etc., where SemiRegular is a type that behaves like a primitive type (e.g. i32) in terms of copying/moving and Regular is a SemiRegular type that also behaves like i32 in terms of comparisons (eq/ord). So I can imagine that we could in Rust have something like:

  • SemiRegular: Copy + Clone,
  • PartRegular: SemiRegular + PartialEq + PartialOrd, and
  • Regular: PartRegular + Eq + Ord,

or something similar to this.

  • lifting lifetime/type parameters to modules
  • often you have large blocks of code that share parameters
  • I've mentioned a few times I think it would be great to float these to the module level

Are you talking about ML parametrized modules? That would certainly help ergonomics but it is also a big new language feature.

Some new features we might consider

This is the perfect list of features Rust still needs, agree with every single one of them. The only ones missing are variadics and, well you mention RFC 1598 but ATC/HKTs, or something in this direction that solves most problems would be nice.

Could we auto-derive a constructor method for structs that just takes all contents as arguments – basically #[derive(new)]?

Yes - crates.io: Rust Package Registry

4 Likes

Not neccessarily – a derived new(..) would be indistinguishable from a manually written one, at least from the outside. So we might require Default impls for all private fields and only use public fields as arguments for our constructor.

Later if we want to require some processing or validation, we can replace the derived constructor with a custom one.

4 Likes

I feel like both of these are part of a more over-arching issue: lifetimes can often be annoying to work with.

From a (shallow) ergonomics standpoint, it would be awesome if Rust could just infer all lifetime parameters, and only shout errors at you when the situation is unresolvable for some reason. However, there is at least one significant problem with doing that: you really want lifetimes to be explicit at API boundaries, because they are actually part of the API.

My understanding is that this is the primary rationale behind requiring explicit lifetime annotations for e.g. function signatures, data structure declarations, etc. All of those things amount to API boundaries, and thus lifetimes are something that should both be thought about and made explicit.

But are functions etc. really Rust's API boundaries? It seems to me that the real API boundaries in Rust are modules. Specifically, public members of modules. It would be amazing if Rust could infer lifetimes in private function signatures, private data structures, private traits, etc. This would make it significantly easier to write e.g. helper functions for code with complex lifetime requirements.

In fact, arguably, maybe the real API boundaries of Rust are crates. Maybe lifetimes could be inferred for all private things within a crate. Then people would only (strictly) need to write lifetime annotations for the publicly exposed parts of crates. This could significantly simplify e.g. writing application code, among other things, while still enforcing that crates are explicit about the lifetimes in their API's and therefore don't accidentally break their API promises.

So, having said all of that... I'm not actually advocating for anything in particular here. There are probably issues or complexities with what I've written above that I'm not aware of. But I wanted to put these ideas out there to maybe spark some discussion of how Rust can make working with lifetimes less of a hassle in a broader sense, rather than just as a set of one-off use cases.

2 Likes

BTW, this design principle also gets in my way when I don't have any API boundaries that I care about when I write binaries. I totally understand API boundary issues for libraries, but the necessary constraint for libraries is a burden when writing a program where everything is under my control, final and de-facto private.

In my programs I often want to use traits simply to reduce repetition, but Rust's explicit API rule requires me to declare everything up front about type parameters (instead of at point of use like in C++), so working with generic types causes explosion of where annotations. Even to add two things together I have to declare use of Add and probably Output of Add too (and I keep confusing syntax of T:Add::Output and Add<Output=T>, etc.)

I also instantly regret whenever I add new fields to structs. If the struct didn't have a reference before, it breaks the whole program. I once added a HashMap to a struct, and had to add : Copy + Eq + Hash + 'static in 14 places in a 150-line file :frowning:

So I'd love if Rust inferred everything that is private/module-scoped, and did what I mean, not what I explicitly declare.

1 Like

Regardless of where you think the real API boundaries are, the more code you have to scan to find out a type, the worse. Code is read more often than it is written. And I’m seeing already now when this feature gets introduced, not specifying the type becomes “idiomatic” rust, clippy starts suggesting to remove types because “its unneccessary” and suddenly most of rust code becomes unmaintainable because you don’t know anymore which types you use and which you don’t.

I want to know which type something has, and fixing the types at the function border is a good compromise IMO.

Maybe some people would like a dialect of rust where all traits, lifetimes and types are elided at compile time even at the API border, where you don’t place braces nor semicolons, but its not something I’d consider useful for my purposes.

Possibly putting the type into the function border can be some task for an IDE, if people really think that not thinking about types makes you more productive (it doesn’t because the less you think about stuff early on, the more you’ll regret it later on), but I don’t want to have to use an IDE to find out which type something has.

If Clippy does that, then it shouldn't, because in many situations you want to keep types for clarity.

Compared to all the times I had to code review "you should be moving that shared_ptr<> in C++", I really like Rust's existing choices here. Copy when it's trivial enough to not care, move by default so the right thing usually happens without thinking, but also really easy to .clone() when you have to. (I needed to, essentially, .clone() a SqlParameter in C# the other day, and it was miserable. C#'s IClonable totally doesn't work, and canonical C# doesn't reliably even have a "copy constructor". Hooray for #[derive(Clone)]!) A "diffuse extra copies all over problem" adds up in cost, but is a royal pain to clean up later. And, extrapolating from shared_ptr/unique_ptr experience, having the copies explicit probably makes a "wait a minute, all these Arcs can just be Boxs" realization easier.

Maybe there's a way that Copy as a concept can be extended to impls using Copy types, though? Like if impl Foo<T> automatically provided impl Foo<&T> for trait Foo<#[in] T> when T:Copy? (So one Add for i32 would be enough, without needing the other three, for example.)

6 Likes

I’ve just returned from having done battle with the great dragon, Borrowchk, this time my foe being aided by deep magic of lifetimes :smiling_imp:. Sadly I was not victorious this time. But one day, one day I shall best him :crossed_swords:.

In this particular experience, I stumbled upon several posts like this and this where the answer to why something was failing the borrow checker was basically “rustc’s borrow checker isn’t smart enough to realize this is actually valid”.

I didn’t see this issue in particular in the roadmap issues and posts that I skimmed through, so I thought I would bring it up. For new people, I wonder how much it would ease the learning curve to just slice away a lot of these false-positives.

I admit, though, I have no concept for how complex a task this really is. Since Rust is the first language in history that I’m aware of that has tried to even do static lifetime analysis, I’m guessing it’s quite complex.

Until then, back to dragon-slaying :bow_and_arrow:.

1 Like

The solution to these problems has the rather jargon name "non-lexical lifetimes." Its very much on the roadmap; there's a tracking issue for it here: Non-lexical lifetimes · Issue #16 · rust-lang/rust-roadmap-2017 · GitHub

2 Likes

Great to hear! Thanks for the link.

Hi, thinking about expressiveness, what comes up to my mind is that it would be great if people can take a well-written casual C program and “transcribe” it into unsafe rust almost line by line, then they can dismiss those unsafety by rewriting code blocks and structures of it in safe rust gradually.

I believe this will raise the adoption rate of rust greatly.

What do you think?

You shuld look at:

Hi,

I’m a total newbie to Rust, and I fell in love with it yesterday, as I tried to create a fast iterator library in C++ for fun, and after I compiled it I realized that the C++ compiler is not able to inline my code.

After that I tried to do the same thing in Rust, until I realized that it’s already part of the core library, and that’s the only way to do things with a vector :slight_smile:

I feel like most of the improvements on the initial list make it harder for me to write performant code as a beginner, as I wouldn’t understand what’s happening underneath the system, so I’m not too keen on for example automatic cloning of Arc, or other low level data structures.

I would much prefer having things like Rayon in the standard library, so that I get parallelized code by default or with very small modification of my high level code.

Also most of the things on the list could be automatically corrected by the compiler, so I would love to see integrated autocorrection of these things, where I can just click on ,Fix’’ in an IDE. That would be the nice balance for me between being a begginer and staying with explicit performant code. A lot of my mistakes were caught by the compiler, the compiler had good suggestions for fixing the problem, but it still took a lot of time to fix those things that the compiler already knew how to fix (except maybe when I was using parathesis with if(…)… the compiler doesn’t try to correct C+±isms well).

2 Likes

Hi, yeah, but Corrode’s functionality is still very very limited - too limited for any practical use though.

Will be happy to see it become better, if possible.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.