Hacker News new | ask | show | jobs
by jeremyjh 3779 days ago
>Code that takes a full structure when it only needs to operate on a part of the structure is badly designed.

No, it is not; this is done all the time with methods and it improves encapsulation - you may not want your clients to be able to decompose your data structures. Do you really mark every member of your data structures as pub??

Sorry but this is a poor ad-hoc defense of an actual annoyance in the borrow-checker.

2 comments

>Code that takes a full structure when it only needs to operate on a part of the structure is badly designed.

I think this was a sensible statement, especially in context; I strongly agree that "this is behind a lot of long-term maintenance messes". And lost performance.

For encapsulation in Rust, traits are used to abstract and separate concerns, but they don't force you to bundle your data into large structures.

And encapsulation isn't an end in itself. Privacy has its uses (maintaining invariants, minimizing the exposed surface area of a library, etc.) but I find often in OO codebases that encapsulation creates its own problems. There is no substitute for careful data-oriented design; no amount of `private` will prevent your teammates from working around or ripping apart your carefully shrink-wrapped objects.

There is certainly some awkwardness in the borrow checker, but also great value.

If you're trying to achieve proper encapsulation, you just have a module that implements some sort of functionality, and shouldn't need to borrow anything from it. The real question is why you're pulling data instead of pushing messages.
This is incorrect. "Sending a message" involves borrowing the data so that the method can run. `foo.bar()` borrows `foo`.
Sorry, I wasn't clear. You are, of course, correct: you're only ever in a position to call a method if you hold a reference to the struct you're calling on.

My meaning was that you should favour a usage pattern that looks like you either move/copy things into the called method, or lend a reference to something you own (which is, presumably, not going to be held on to for very long), and then you're either given ownership of whatever return value you get, or get a reference whose lifetime depends on the arguments you passed in (but not the object itself). All of this ends up being quite clean, and you don't end up tying yourself into a borrowing knot.

You do end up in a weird place when your methods return references to fields of the owning object. When that happens, you're restricted in what you can do with the owning object until the reference goes out of scope. Rust mutexes are implemented precisely like that, which highlights what sort of behaviour you're getting from this usage pattern.

The former provides better encapsulation and more closely resembles the message-passing approach to OOP, whereas the latter pattern is not only not very ergonomic, it's quite indicative of poor encapsulation (because you're, by necessity, asking for internal state).

Here is the issue: `self.foo.bar(self.baz())` is an error if `foo.bar()` mutates foo, even if `baz()` doesn't touch `foo` and even if `baz()` doesn't return a reference. This is because borrowck doesn't properly understand that baz will be evaluated before bar, and can't distinguish which elements of a struct are accessed by that struct's methods. Both of these are problems that can be solved, and neither of them is actually promoting good practice in my opinion.

All it does is force you to use unnecessary temporaries, like `let baz = self.baz(); self.foo.bar(baz)`

Yeah, this is basically a borrowck "bug" which will probably be fixed post-MIR.

Note that there are cases where such code is invalid even with the temporary, and they can be related to Demeter. Ish. Also to API contracts; the guarantee should be embedded in the signature (so changing the internals shouldn't cause its usage to stop compiling), which is unweildy to do.