I already have a lot of experience with C# back in the day, and after having done quite a bit of research, I settled on Rust (for it's versatility, speed, strong type & memory usage checking, and excellent tooling) and OCaml (for its functional programming strength, expressive typing, use for DSLs, and stability). I'd like to use Rust as my general purpose language, and Ocaml to write Rust code generators and DSLs.
However, the more I learn Rust, the more I am baffled by choices made regarding syntax and type/memory use checking. The syntax seems excessively redundant and low level, with implementation details leaking all over the place. One example is at [1]. Another example is at [2], where there is this quote:
> ...why should a reference to the first element care about changes at the end of the vector? This error is due to the way vectors work: because vectors put the values next to each other in memory, adding a new element onto the end of the vector might require allocating new memory and copying the old elements to the new space, if there isn’t enough room to put all the elements next to each other where the vector is currently stored. In that case, the reference to the first element would be pointing to deallocated memory. The borrowing rules prevent programs from ending up in that situation.
The explanation makes sense, but the larger question is: why should low level implementation details affect syntax at this level? Why can't references be updated automatically when a vector is moved?
I also don't get this whole complexity about string handling (e.g., string slices vs regular strings). I've read the docs several times and I still don't really get why this design is necessary.
The whole language just seems unnecessarily complicated, quite a contrast to the cleanness and elegance of Ocaml. Can't we have a simpler syntax and design layered on top, probably with a GC as an option?
[1] https://stackoverflow.com/questions/68725942/is-a-mut-on-a-moved-function-parameter-leaking-implementation-details-to-the-f
[2] https://doc.rust-lang.org/stable/book/ch08-01-vectors.html
https://doc.rust-lang.org/beta/embedded-book/static-guarante...
There are of course ways to get garbage collection, ignore these details, etc. in Rust with use of appropriate abstractions. But Rust may not be the best option for every application.
Try to think about what it would take to implement such a thing. How do you update references that are stored in registers? The stack? The heap? What if the references are self–referential? What if multiple threads have references to the same object? How do you ensure that all threads with a reference to the same object are all updated in sync with each other? Does your design require that every dereference go through a lock?
Where will this code live? It has to operate at run time, of course, but where does the compiler insert it? Does it get inserted into the vector methods, or into any method that might reallocate something? How does the compiler tell when that will happen?
Is it worth adding a runtime to Rust, when one of the key design decisions was to remove anything that required a runtime?
> I also don't get this whole complexity about string handling (e.g., string slices vs regular strings).
The big difference between a &str and a String is that the String has methods that can reallocate the backing buffer, while &str does not. The &str has just enough information to give you read–only access to a sequence of (utf–8 encoded) characters, but not enough to let you write to them (because writing might cause reallocating).
Think about what the compiler does to string literals. If you write "foo".to_string() in your code, the bytes 0x66 0x6F 0x6F have to live somewhere in the executable, and they have to be put into memory at an address the compiler can predict in advance. Typically that means the compiler will put them into the data segment of the executable. The program loader will copy that segment into memory, and then update the program’s global offset table using the starting memory address of the executable. Once that is done the program can actually be started, and the &str for the literal can be loaded out of the GOT.
Converting that to an owned String using the to_string() method has to copy from the data segment into a heap–allocated buffer, because the data segment is probably read only and anyway mutations might require reallocation (which cannot be done inside the data segment).
I suppose the Rust language could have been simplified by making the compiler automatically convert all string literals into Strings for you. This would reduce the number of things that need to be taught to new users, which is nice, but it would be rather unfortunate for performance in programs that don’t actually need to mutate those literals.
> The whole language just seems unnecessarily complicated, quite a contrast to the cleanness and elegance of Ocaml. Can't we have a simpler syntax and design layered on top, probably with a GC as an option?
Ocaml already exists. You can write programs in it if you want. In fact, I encourage it! Most programs should not be written in Rust, even though Rust has a lot of hype.
Languages like Ocaml get their cleanness and elegance by having a runtime that smooths over the complexities of the hardware in order to make the programmer as productive as possible. This is a good thing, because most programs do not require absolutely the best performance. Instead, most of the cost of most programs is the time and brain–power needed to write them. Thus, most programs should be written in a high–level language with garbage collection precisely because they minimize the amount of effort the programmer needs to use, maximizing productivity.
Rust is for those programs that benefit greatly from lower–level access to the hardware, and for which it is worth spending the extra time and brain–sweat on. The advantage of Rust is that C++ doesn’t have sum types, automatic closures, and memory safety.
Rust is an upgrade when the alternative is C or C++, but a downgrade when you could just write your program in Ocaml or Lisp or Javascript.