How should one best expose such algorithms in Rust?
Naively, it seems to me that the most idiomatic solution is moving the input in, consuming it as fit, and then moving the output out. This seems to correspond best to what is actually going and thus should allow the user (and the compiler) to take the best decisions.
If the most convenient and most efficient APIs do not coincide, you should expose both. You shouldn't be making trade-offs on behalf of the user.
If your problem most naturally lends itself to an in-place algorithm, then make that the fundamental implementation. Then, do provide by-value and/or by-reference functions as well, simply implemented in terms of the fundamental one. They are not boilerplate, they remove boilerplate at the call site (you only write your library once, but it will have many users). People don't always want to have to pre-allocate buffers, that's a lot of noise.
rug, which does big-integer arithmetic using gmp, is pretty similar. It exposes several different versions of very similar functions based on whether they move or borrow one of the operands. It can be a little awkward but I think it's a good compromise between readability and efficiency.
Is there any established convention for naming in such situations? Would it be uncontroversial to just slap a _ref suffix at the end of the reference variants (or _mut in the case it's a mutable reference)?
Thanks! My confusion stemmed from me wrongly believing that the most idiomatic interface to a function that modifies its argument (for example matrix QR decomposition) would be move-in-move-out.
This is actually not true. It would mean that someone holding only a mutable reference to a value (and not owning it), could not use that function.
It seems that it would be hypothetically possible to remove mutable references from the Rust language. Then functions taking a mutable reference would have to resort to move-in-move-out. But mutable references seem to exist precisely as a less cumbersome alternative to this pattern.
It seems to me that the right way to think about ownership is “who is responsible for destroying the thing”.
Thanks for the example. I understand their difference and the performance vs. convenience tradeoff with regard to the output buffer/string. However, why does io::read_to_string() consume the reader while Read::read_to_string only requires a mutable reference to it? This is not related to the aforementioned tradeoff.
This is not possible — or at least, you would have to invent a new mechanism to replace it — because if:
move out of some place that is not a local variable (is some struct field or similar),
call a function (with the value moved),
and that function then panics,
then you have a problem because the place now contains de-initialized memory — the ownership of the value was transferred to the function which didn't return it. You'd need some way to automatically re-initialize the place, or to always have a placeholder value to put in temporarily (which implies that all types need placeholder values, which weakens the type system).
It's not always possible if, for example, function projects from the &mut Struct to &mut Field - in your description it would mean that the function would destroy the struct and return only the field, which is not always possible.
No, not really. Indirection is a fundamental necessity for all sorts of data structures and algorithms. How do you propose a subrange of an array would be overwritten, for example? You can't just move out of the middle of the array (not only because you'd need to replace a reference-to-slice, a DST, which is not possible today by value, but also because dynamic partial moves like this would almost always be impossible to keep track at compile-time, exactly because they are dynamic).
If R: Read then &mut R: Read as well. Therefore io::read_to_string()doesn't (need to) consume the reader. Here, taking the reader by-value makes the API simpler and more general. If that function took a mutable reference, then only references could ever be passed. With the actual signature, references and non-reference values can be passed just as well.
In contrast, the io::Read::read_to_string() method needs to take a mutable reference because most of the time, you don't want to transfer ownership of a writer; instead, you want to reuse it. Making the trait method accept a mutable reference makes the auto-referencing mechanism of method call syntax kick in, providing a convenient and correct default. Had the method been declared as read_to_string(self) -> io::Result<String>, the user would usually be forced to take an explicit reference and write (&mut r).read_to_string(), which is just plain ugly.
You seem to be confused about generic type notation. A generic type variable R can stand in for any type. It doesn't mean "a non-reference type". Just like a variable x in algebra can stand in for "any number". If it's a result of adding two other numbers, it's still just a number and can be denoted with x, you don't have to write it as a + b.
Indeed. I only had calling of functions with local variables and temporary values in mind. For that application, I guess that move-in-move-out is semantically equivalent to taking a mutable reference.
Thanks for clarifying this. I have a lot of experience with C++ templates (from the time when these were untyped, i.e. before concepts), but still very little with Rust generics. Reading R: Read indeed tricked me into believing that R may not be a reference.
You mean "you don't want to transfer ownership of a reader", right?
I must be confused again: had the trait method been defined to take self (read_to_string(self) -> io::Result<String>) and not a reference, how would it be possible to evoke it for a reference: (&mut r).read_to_string()?
In the implementation of Read for &mut R where R: Read, Self (which is the type of a self receiver) is &mut R.
So if you have some owned reader: R and a method that takes self, you need to pass &mut reader and not reader to avoid giving away ownership. Given how method resolution works, you would need the unergomatic (&mut reader).method(...) (or to not use method call syntax).
It works in the same way as C++ templates. If you have a template<typename T>, you can make T a pointer or a non-pointer. "All types" include pointers.
Again, because R: Read implies &mut R: Read. Substitute Self = &mut R and now self has type &mut R. It's exactly the same thing. self is simply sugar for self: Self where Self is once again a type variable denoting an arbitrary type (the implementor).