I know that Rust provides some additional compile-time checks because of its stricter type system, but it doesn't come for free - it's harder to learn and arguably to read
I know that Rust provides some additional compile-time checks because of its stricter type system, but it doesn't come for free - it's harder to learn and arguably to read
Ownership/borrowing clarifies whether function arguments are given only temporarily to view during the call, or whether they're given to the function to keep and use exclusively. This ensures there won't be any surprise action at distance when the data is mutated, because it's always clear who can do that. In large programs, and when using 3rd party libraries, this is incredibly useful. Compare that to that golang, which has types for slices, but the type system has no opinion on whether data can be appended to a slice or not (what happens depends on capacity at runtime), and you can't lend a slice as a temporary read-only view (without hiding it behind an abstraction that isn't a slice type any more).
Thread safety in the type system reliably catches at compile time a class of data race errors that in other languages could be nearly impossible to find and debug, or at very least would require catching at run time under a sanitizer.
Basically, I don't need ownership, if I don't mutate things. It would be nice to have ownership as a concept, in case I do decide to mutate things, but it sucks to have to pay attention to it, when I don't mutate and to carry that around all the time in the code.
Non-owning non mutating borrow that doesn’t require you to clone/copy:
fn foo(v: &SomeValue)
Transfer of ownership, no clone/copy needed, non mutating: fn foo(v: SomeValue)
Transfer of ownership, foo can mutate: fn foo(mut v: SomeValue)
AFAIK rust already supports all the different expressivity you’re asking for. But if you need two things to maintain ownership over a value, then you have to clone by definition, wrapping in Rc/Arc as needed if you want a single version of the underlying value. You may need to do more syntax juggling than with F# (I don’t know the language so I can’t speak to it) but that’s a tradeoff of being a system engineering language and targeting a completely different spot on the perf target.Ah, you are confused on terminology. Borrowing is a thing that only happens when you make references. What you are doing when you pass a non-copy value is moving it.
Generally, anything that is not copy you pass to a function should be a (non-mut) reference unless it's specifically needed to be something else. This allows you to borrow it in the callee, which means the caller gets it back after the call. That's the workflow that the type system works best with, thanks to autoref having all your functions use borrowed values is the most convenient way to write code.
Note that when you pass a value type to a function, in Rust that is always a copy. For non-copy types, that just means move semantics meaning you also must stop using it at the call site. You should not deal with this in general by calling clone on everything, but instead should derive copy on the types for which it makes sense (small, value semantics), and use borrowed references for the rest.
What I would prefer is, that Rust only cares about whether I use it in the caller after the call, if I pass a mutable value, because in that case, of course it could be unsafe, if the callee mutates it.
Sometimes Copy cannot be derived and then one needs to implement it or Clone. A few months ago I used Rust again for a short duration, and I had that case. If I recall correctly it was some Serde struct and Copy could not be derived, because the struct had a String or &str inside it. That should a be fairly common case.
&str is Copy, String is not.
let v = SomeValue { ... }
foo(&v);
foo(&v);
eprintln!("{}", v.xyz);
You have to take a reference. I'm not sure how you'd like to represent "I pass a non-reference value to a function but still retain ownership without copying" - like what if foo stored the value somewhere? Without a clone/copy to give an isolated instance, you've potentially now got two owners - foo and the caller of foo which isn't legal as ownership is strictly unique. If F# lets you do this, it's likely only because it's generating an implicit copy for you (which Rust is will do transparently for you when you declare your type inheriting Copy).But otherwise I'm not clear what ownership semantics you're trying to express - would be helpful if you could give an example.
Note that calling by value is expensive for large types. What those other languages do is just always call by reference, which you seem to confuse for calling by value.
Rust can certainly not do what you would prefer. In order to typecheck a function, Rust only needs the code of that function, and the type defitions of everything else, the contents of the functions don't matter. This is a very good rule, which makes code much easier to read.
How do they do that without either taking a reference or copying/cloning automatically for you? Would be helpful if you provide an example.
``` fn operate_on_a(a: A) -> A { // do whatever as long as this scope still owns A a } ```
I might be wrong what they actually do though. It seems I merely dislike the need to specify & for arguments and then having to deal with the fact, that inside procedures I cannot treat them as values, but need to stay aware, that they are merely references.
The nice thing about value semantics is they are very safe and can be very performant. Like in PHP, if we take array that's a copy. But not really - it's secretly COW under the hood. So it's actually very fast if we don't mutate, but we get the safety of value semantics anyway.
But Rust always moves by default when assigning so I’m not sure what your complaint is. If the type declares it implements Copy then Rust will automatically copy it on assignment if there’s conflicting ownership.
My complaint is that because moves are the default, member access and container element access typically involves borrowing, and I don't like dealing with borrowed stuff.
It's a personal preference thing, I would prefer that all types were copy and only types marked as such were not.
I get why the rust devs went the other way and it makes sense given their priorities. But I don't share them.
Ps: most of the time I write python where references are the default but since I don't have to worry about lifetimes, the borrow checker, or leaks. I am much happier with that default.
In Rust, "Copy" means that the compiler is safe to bitwise copy the value. That's not safe for something like String / Vec / Rc / Arc etc where copying the bits doesn't copy the underlying value (e.g. if you did that to String you'd get a memory safety violation with two distinct owned Strings pointing to the same underlying buffer).
It could be interesting if there were an "AutoClone" trait that acted similarly to Copy where the compiler knew to inject .clone when it needed to do so to make ownership work. That's probably unlikely because then you could have something implement AutoClone that then contains a huge Vector or huge String and take forever to clone; this would make it difficult to use Rust in a systems programming context (e.g. OS kernel) which is the primary focus for Rust.
BTW, in general Rust doesn't have memory leaks. If you want to not worry about lifetimes or the borrow checker, you would just wrap everything in Arc<Mutex<T>> (when you need the reference accessed by multiple threads) / Rc<RefCell<T>> (single thread). You could have your own type that does so and offers convenient Deref / DerefMut access so you don't have to borrow/lock every time at the expense of being slower than well-written Rust) and still have Python-like thread-safety issues (the object will be internally consistent but if you did something like r.x = 5; r.y = 6 you could observe x=5/y=old value or x=5/y=6). But you will have to clone explicitly the reference every time you need a unique ownership.
At least as long as I can afford it performance wise. Then borrowing it is. But I would prefer the default semantics to be copying.
How could/would the language know when you can and can't afford it? Default semantics can't be "copying" because in Rust copying means something very explicit that in C++ would map to `is_trivially_copyable`. The default can't be that because Rust isn't trying to position as an alternative for scripting languages (even though in practice it does appear to be happening) - it's remarkable that people accept C++'s "clone everything by default" approach but I suspect that's more around legacy & people learning to kind of make it work. BTW in C++ you have references everywhere, it just doesn't force you to be explicit (i.e. void foo(const Foo&) and void foo(Foo) and void foo(Foo&) all accepts an instance of Foo at the call site even though very different things happen).
But basically you're argument boils down to "I'd like Rust without the parts that make it Rust" and I'm not sure how to square that circle.