4 ms·
For the disjoint field issues raised, it’s not that the borrow checker can’t “reason across functions,” it’s that the field borrows are done through getter func
by alilleybrinker 1y ago
For the disjoint field issues raised, it’s not that the borrow checker can’t “reason across functions,” it’s that the field borrows are done through getter functions which themselves borrow the whole struct mutably. This could be avoided by making the fields public so they can be referenced directly, or if the fields needs to be passed to other functions, just pass the the field references rather than passing the whole struct.
There are open ideas for how to handle “view types” that express that you’re only borrowing specific fields of a struct, including Self, but they’re an ergonomic improvement, not a semantic power improvement.
- mirashii 1y ago> For the disjoint field issues raised, it’s not that the borrow checker can’t “reason across functions,” it’s that the field borrows are done through getter functions which themselves borrow the whole struct mutably Right, and even more to the point, there's another important property of Rust at play here: a function's signature should be the only thing necessary to typecheck the program; changes in the body of a function should not cause a caller to fail. This is why you can't infer types in function signatures and a variety of other restrictions.
- sowbug 1y agoSee Rust's golden rule: https://steveklabnik.com/writing/rusts-golden-rule https://steveklabnik.com/writing/rusts-golden-rule
- majormajor 1y agoThis seems to be a golden rule of many languages? `return 3` in a function with a signature that says it's going to return a string is going to fail in a lot of places, especially once you exclude bolted-on-after-the-fact type hinting like what Python has. It's easier to "abuse" in some languages with casts, and of course borrow checking is not common, but it also seems like just "typed function signatures 101". Are there common exceptions to this out there, where you can call something that says it takes or returns one type but get back or send something entirely different?
- mirashii 1y agoMany functional and ML-based languages, such as Haskell, OCaml, F#, etc. allow the signature of a function to be inferred, and so a change in the implementation of a function can change the signature.
- mirashii 1y ago> Are there common exceptions to this out there, where you can call something that says it takes or returns one type but get back or send something entirely different? I would personally consider null in Java to be an exception to this.
- tnh 1y agoIn C++, the signature of a function template doesn't necessarily tell you what types you can successfully call it with, nor what the return type is. Much analysis is delayed until all templates are instantiated, with famously terrible consequences for error messages, compile times, and tools like IDEs and linters. By contrast, rust's monomorphization achieves many of the same goals, but is less of a headache to use because once the signature is satisfied, codegen isn't allowed to fail.
- spacechild1 1y ago> In C++, the signature of a function template doesn't necessarily tell you what types you can successfully call it with, nor what the return type is. That's the whole point of Concepts, though.
- aw1621107 1y agoConcepts are basically a half solution - they check that a type has some set of properties, but they don't check that the implementation only uses those properties. As a result, even with concepts you can't know what types will work in a template without looking at the implementation as well. Example [0]: #include <concepts> template<typename T> concept fooable = requires(T t) { { t.foo() } -> std::same_as<int>; }; struct only_foo { int foo(); }; struct foo_and_bar { int foo(); int bar(); }; template<fooable T> int do_foo_bar(T t) { t.bar(); // Compiles despite fooable not specifying the presence of bar() return t.foo(); } // Succeeds despite fooable only requiring foo() template int do_foo_bar<foo_and_bar>(foo_and_bar t); // Fails even though only_foo satisfies fooable template int do_foo_bar<only_foo>(only_foo t); [0]: https://cpp.godbolt.org/z/jh6vMnajj https://cpp.godbolt.org/z/jh6vMnajj
- JoshTriplett 1y agoExactly. We've talked about fixing this, but doing so without breaking this encapsulation would require being able to declare something like (syntax is illustrative only) `&mut [set1] self` and `&mut [set2] self`, where `set1` and `set2` are defined as non-overlapping sets of fields in the definition of the type. (A type with private fields could declare semantic non-overlapping subsets without actually exposing which fields those subsets consist of.)
- rstuart4133 1y agoYou could it in a more limited fashion by allowing fields of a struct to be declared "const", which would have similar semantics to Java's final. If you add the ability to return const references, you get the ability to have read only and mutable references to stuff within a struct co-exist. For example, this won't compile: struct Something { z: usize } struct Foo<'a> { x: usize, y: &'a Something } impl<'a> Foo<'a> { fn bar(&mut self) -> &Something { let something = self.bar(); self.x += something.z; something } } But if you could tell the borrow checker the mutable borrow of self can never modify z, then it would be safe. This would achieve that: struct Something { z: usize } struct Foo<'a> { x: usize, y: &'a const Something } impl<'a> Foo<'a> { fn bar(&mut self) -> &const Something { let something = self.bar(); self.x += something.z; something } } I've now had several instances where they would have let me win a battle with the borrow checker succinctly rather than the long work around I was forced to adopt. Const struct members allow you implement read only fields with having to hide them, and provide getters is icing on the cake.
- saghm 1y agoIt's super easy to demonstrate your point with the first example the article gives as well; instead of separate methods, nothing prevents defining a method `fn x_y_mut(&mut self) -> (&mut f64, &mut 64)` to return both and use that in place of separate methods, and everything works! This obviously doesn't scale super well, but it's also not all that common to need to structure this way in the first place.