If a reference is part of the type system, rather than an argument annotation, you can pass everything by value (as references are values).
I may be missing something, but as far as I can tell, a reference is part of Rust's type system. You can build a struct with reference members (see e.g. iterators), or parametrize a generic entity with a reference. You can build types which contain references, like iterators, and instances of these types are values (with a finite lifetime). Could you clarify what are you thinking about here?
My mistake, you are right. It was the specialisation rules for type classes that stopped me treating values and references uniformly. But in this case your comment here is misleading:
The problem here is that there are good use cases both for moving input into a function, and for returning a reference, which would be hard to emulate if both of these options were disallowed.
And my comment about too many passing conventions was wide of the mark too. Essentially there is only one passing convention, and that is pass by value. A reference type &A
is just an example of a value no different to an Int (ignoring lifetimes). The compiler chooses to copy or move depending on whether Copy
is defined.
So I don't think that is part of the problem, and was a bit of a red-herring.
Ah, yes, sorry about this. Coming from a C++ background, I have learned to keep a strong mental distinction between passing parameters/returning results by value versus by reference, and I keep thinking in these terms when writing Rust code even though Rust's references are really first-class values.
So that leaves me with the lifetime annotations, the type system, and exclusive write references as my main problems with Rust. Also a failure to distinguish between immutability and read-only references. If lifetimes can be fully inferred then I find them less of a problem.
So, basically, lifetimes are inferred when...
- They don't affect a function's interface semantics (e.g. a function with no output, or which returns a value of unbounded lifetime like an i32)
- There is a good default choice based on established usage (either the function only has one input, in which case the lifetime of the output is inferred to be that of the input, or it is an object method, in which case the lifetime of the output is inferred to be that of the object ("self"))
In other cases, Rust does not attempt to impose a default, because as you saw in my example above (a function with two input references returning one reference), there may be no obvious good answer, and the more things Rust makes implicit based on arbitrary criteria, the more cognitive load it imposes on programmers. Implicit vs explicit is a difficult balance.
Regarding exclusive write references, it's certainly one of the most opinionated features of Rust, but I also think that although it lacks polish (e.g. disallows &mut to two independent container elements, which is safe), it is a very sensible one in general. Let's see what it buys us:
- Programmers do not need to worry about aliasing ever again. Screw memcpy vs memmove, iterator invalidation, spooky action at a distance where touching one variable has an impact on another... life just became so much simpler.
- Compilers do not need to worry about aliasing either, so you can scrap that broken restrict keyword and enjoy more reliable optimization "for free". Better autovectorization and code reordering, data that stays in registers instead of taking round trips through caches and main memory, etc.
- And, of course, there is also everyone's favorite: data races. Gone for good. People can finally stop regarding multithreading as black voodoo magic and start realizing that every CPU is multicore today and punting on threads is not a reasonable option.
Regarding the type system and the failure to distinguish between immutability and read-only references, I do not understand your arguments here well enough to have a discussion. Could you elaborate a bit on these two?
In other cases, Rust does not attempt to impose a default, because as you saw in my example above (a function with two input references returning one reference), there may be no obvious good answer, and the more things Rust makes implicit based on arbitrary criteria, the more cognitive load it imposes on programmers. Implicit vs explicit is a difficult balance.
There needs to be a principal type for types including lifetimes.
Regarding exclusive write references, it's certainly one of the most opinionated features of Rust, but I also think that although it lacks polish (e.g. disallows &mut to two independent container elements, which is safe), it is a very sensible one in general.
Or the compiler can track the aliases. Rust has an affine type system, but you could use a linear-alias type system.
Regarding the type system and the failure to distinguish between immutability and read-only references, I do not understand your arguments here well enough to have a discussion. Could you elaborate a bit on these two?
Mutability is a property of an object, whereas Readable, Writable, are properties of a reference to an object (a reference is a kind of access token, see capability based security). I think there needs to be a distinction between l-values and r-values as well. A 'value' is immutable, and cannot be referenced (is an r-value, has no location / has referential transparency), a 'container' is mutable, and can be referenced (is an l-value, has a location / does not have referential transparency).
Thanks for these clarifications. Unfortunately, they use too much type theory jargon for me to understand them (I'm just a language user, I only care about the theory behind it when it gets in my way ^^), so you will need to either reword this into a form that more people can understand or hope that a specialist passes by.
If you're specifically looking for people more familiar with the type theory behind Rust's design, may I suggest https://internals.rust-lang.org/ as a place where I think you would have higher chances of finding them?
I was just responding to the question "why not use rust" with the reasons why I don't use rust more. All the terms are googleable, it could take a book to explain everything, and I am sure others explain much better than I could.
A short summary is that alias types encode aliasing in the type system, so instead of tracking lifetimes, we track aliases. There exist type inference algorithms for alias types. Memory safety then becomes checking if any aliases outlive the scope of the object they reference.
Edit: A more concise summary of all my points might be, that by using a more complex type-system, the language can become easier to use, because things behave more like you would expect without needing explicit annotations.
Well this made C++ more and more complex and unsound (in my opinion).
I agree, lifetimes are complex because memory management is complex.
If memory was easy, we wouldn't have all the CVE's based on double-free, buffer overrun or out-of-bound reads.
Every time the Rust compiler "complains" about a missing lifetime annotation, I reckon that would have been a subtle (or less subtle) segfault in C. A segfault that probably occurs only on Saturdays that are on uneven days of the month, and require two years to finally track down, after which they'll have cost the company thousands of Euro, and the programmer dozens of overtime weekends to debug in production.
After that, it sounds a lot less like "complaining", and more like a parent stopping their toddler from crawling into the trash compactor...
Today I read about how the NotPetya malware is currently costing the Rotterdam Port container terminal β¬100.000.000 an hour in lost productivity... (Rotterdam port, second-largest harbour in the world, "gateway to Europe", terminal operated by Danish shipping Moloch, and NotPetya victim, Maersk). Not a single container could move for hours, because the planning systems were ransomed.
I can't help but wonder if that could have happened if we had had Rust twenty years earlier, and buffer overflows had been a thing of the past...
I agree!
The thing is, Rust's safety can only be guaranteed because it enforces it everywhere, and only offers the safe abstractions (ok, ok, except unsafe
).
Such guarantees can only be obtained by taking features away, not by adding more warts.
You'd have to forbid large sections of the C spec before you could get Rust's safety in C.. (basically, anything to do with malloc, free, pointers of any kind and probably even templates?) By then, you've broken backwards compatibility so much, you might as well start over (and call it Rust )
Very well said. C++ did a lot for the world, but we really should be moving on.
Lets not be in too much of a hurry to replace Rust and build a whole new set of libraries, tools and bindings. I agree, it's not close to an ideal language, but it's pretty good.
I wouldn't call "I hope to live to see the day" a hurry. If anything, it's overly optimistic.
C++ is better than writing raw assembly, very much true!
And if Rust can make C++, C, D, and the entire rest of the programming alphabet, better by leading-by-example, that's still a win for everyone!
I've known C++ for 11 years, but Rust for about a month or so. There are times where I've ditched it, only to come back the next day, but I think my time with C++ is limited. I tried C to make it simpler, but I've found Rust to be better, and yet avoid C++'s bloat.
- Lopping methods together with variables (C++ classes) makes me cringe. I like how Rust eliminates things like VMTs and other overhead.
- C string and array management makes me bite my knuckles. I'm glad Rust keeps C's simplicity yet allows things like vectors and auto-managed strings
- Rust macros > C macros
- Tagged unions is where I really salivated. Sure C has unions and anonymous embedded structs, but it's not as pretty.
- Closures and generics are a must for me and I'm glad I don't need to resort to C++ anymore to use them.
- Rust's inline assembly macro is interesting to look into. I'd certanly prefer that over GCC or MSVC's variants.
- 128-bit integer types on the horizon will be great for implementing GUIDs
- 'const fn' will eliminate some uses of the include_bytes!() macro I hope. C++'s constexpr was really neat.
My complaints though:
- Built-in struct bitfields are missing. If possible, seeing as type is ignored with bitfields, perhaps a BitVec<#> type once generic integer args are implemented?
- Variadic generics would also help. I am unsure if Rust even does var args right now.
- Unsafe { } still has restrictions (ie. can't pointer-cast-deref f32 to u32). Sure there's mem::transmute() but it can get problematic when you have an algorithm like this: https://github.com/mach-kernel/KDX-Maya/blob/master/DecKDX/kdxalgo.h so it would be nice for people like me who fully know they might shoot themselves in the foot to be able to tell rustc to shut up in some cases.
- Ownership/borrowing can be a hassle sometimes. I've found myself cloning a few things whereas in C/C++ I don't need those expensive operations. Borrowing a mutable closure for recursion is one thing I've found to be utterly painful.
It's worth pointing out that there was a lot of work on Concepts starting in the early 2000's intended for inclusion in the original C++0x. As we all know, Concepts didn't make it into C++11, which was 6 years ago now. And there are mentions of Concepts as C++ template constraints dating back to at least 1998.
Only in C++-land could 19 years for a feature be considered not enough time... (To me, that indicates very deep, fundamental problems--both technical and organizational). But maybe 22 years will get the job done.
Just a few comments:
IIRC, there is a long-term plan for variadic generics, but the lang team has not yet reached agreement on how to make it ergonomic (probably a worthwhile concern when one looks at horrors like std::index_sequence in C++), and has more urgent things on the table when it comes to generics such as non-type parameters (which enable, for example, being generic over fixed-length arrays, a feature that Rust is sorely missing today).
No varargs for you yet either. I think they would eventually have to be implemented via variadic generics in order to be type-safe, but don't quote me on this
Actually, this one is undefined behaviour in C and C++ too these days (it is called the "strict aliasing rule"), and the reason it is is because it makes it too difficult for compilers to keep data in CPU registers for an extended period time, forcing many round trips through caches and memory.
Modern GCC should warn you when you use this kind of coding pattern in code that is compiled at -O2 or above, and its optimizer may randomly trash code that does so unless you explicitly tell it to produce inefficient code with "-fno-strict-aliasing".
This is not a very satisfactory solution, but whenever you hit the limits of rustc's current compile-time borrow checker and cannot easily work around them, you can take an escape hatch to run-time borrow checking with RefCell.
Micro nitpick: rust supports variadic arguments for extern functions, so you can call printf
and friends:
extern "C" {
fn variadic(foo: *const char, ...);
}
Well what I'm doing is recursively passing a reference in a search algorithm
Would you have a minimal code snippet that you can share? I find it easier to reason at this level...