There's a massive Terra Incognita to explore between Rust on one side and Python on the other side (just to pick two extremes).
It's not "2 out of 3", it's a triangle where a language can pick a sweet spot anywhere within the triangle (and ideally, it's not a "sweet spot" either, but more like a "sweet area" where the programmer can pick an actual spot within that area defined by the language).
That sort of extreme flexibility simply does not exist. I mean it does but then you are firmly in the dynamic languages territory and you are forgoing any hope for close-to-the-metal performance.
IMHO it does and its not restricted to dynamic languages (like JS or Python), look at this Zig function signature for instance (just an example from my current dabbling):
In practice this looks and feels like dynamic typing, yet when looking at the compiler output it still resolves to optimal code (since it's "compile-time dynamic typing" not "run-time dynamic typing", but the difference in practice is surprisingly small).
Well you can make OCaml and Rust look like dynamic typing as well by omitting type signatures and squeezing the type inference engine as much as you can but I was under the impression that you have more asked about something that is very relaxed in terms of upfront requirements and be able to tighten it up later?
That's why I claimed that no such language exists.
I was thinking more in terms of "language surface" but it didn't come across, C and Zig have a fairly small language surface (Zig a bit bigger than C), simple language primitives that can be combined quite freely and without much restrictions, but at the same time not carrying a lot of semantics the compiler could use to ensure safety (ignoring the "sloppiness" design warts of C though, like implicit type conversions, allowing accidential uninitialized data, or inverted defaults (mutable vs immutable) - these things are obvious problems but cannot be easily fixed in C or C++ because of backward compatibility requirements - they have been fixed in Zig though).
Rust is quite the opposite, more primitives that carry semantics (most of those in the stdlib though), a stronger but also more rigid type system, but those are pretty much needed for the compiler to guarantee safety.
The million dollar question is of course, can there be a more relaxed Rust with the same safety and performance guarantees, maybe by "squeezing the type inference engine" even more (and letting Rust look or somehow extract information across crate boundaries)? And do Rust programmers even see this as desirable, or are they mostly comfortable in their current sweet spot of the triangle?
I agree that your million dollar question is very good and relevant. Rust can definitely be improved -- async is sticking out as a sore thumb still and it requires a lot of ad-hoc knowledge that's not at all intuitive -- but I'd still venture to say it does pretty well in many areas.
Rust absolutely is not the end state of programming languages but I feel in many ways it's quite ahead than most of everything else. But yes, we still need something a tad better than it.
Again, restrictions that are forced you for a price of safety are one thing.
But what I'm complaining about are restrictions that don't have to be there to get borrowchecker working, but rather are there because designers arbitrary decided "it's better this way".
All AFAIK, since I only dabble occasionally in Rust:
The borrow checker works on "struct granularity", but it would be much more flexible and convenient if borrowing would work on memory location granularity (for instance passing a struct reference into a function "taints" the entire struct as borrowed, even if that function only accesses a single item in the borrowed struct - this 'coarse borrowing' restriction then may lead to all sorts of workarounds to appease the compiler, from 'restructuring' your structs into smaller pieces (which then however may fit one borrowing situation, but not another), or using 'semantic crutches' like Rc, Cell or Box.
There are also related restrictions about function call barriers. AFAIK the Rust compiler cannot "peek into" called function bodies to figure out what's actually going on inside those functions (and that information would be very valuable for fine-grained borrow checking), it can only work with the information in the function signature.
Again, disclaimer: take this with a grain of salt since I'm not a daily Rust user, but this is how I understood why Rust feels so restrictive.
> The borrow checker works on "struct granularity", but it would be much more flexible and convenient if borrowing would work on memory location granularity (for instance passing a struct reference into a function "taints" the entire struct as borrowed, even if that function only accesses a single item in the borrowed struct - this 'coarse borrowing' restriction then may lead to all sorts of workarounds to appease the compiler, from 'restructuring' your structs into smaller pieces (which then however may fit one borrowing situation, but not another), or using 'semantic crutches' like Rc, Cell or Box.
That's valid, thanks for pointing it out. I seem to recall the team lately mentioning they are starting to consider fixing that. And yes that's a real productivity killer, happened to me as well in the past.
> There are also related restrictions about function call barriers. AFAIK the Rust compiler cannot "peek into" called function bodies to figure out what's actually going on inside those functions (and that information would be very valuable for fine-grained borrow checking), it can only work with the information in the function signature.
I don't think it's so much "can not" as it is "will not". Allowing the function signature to be determined by the body can lead to accidentally breaking callers by changing the body.
That, at least, is consistent with other parts of the signature: the input/output types and how the lifetimes of input/output references are related.
It's not "2 out of 3", it's a triangle where a language can pick a sweet spot anywhere within the triangle (and ideally, it's not a "sweet spot" either, but more like a "sweet area" where the programmer can pick an actual spot within that area defined by the language).