Hello, eveyone, I got a problem and need your help.
For a few days I'm struggling to make generic functions that would work for both borrowed and owned types. As I investigated, some people suggest to use Borrow or AsRef traits to convert function argument to reference, and implement function to work with references, which is totally ok for me. So consider this simple example (playground link)
use std::borrow::Borrow;
use std::ops::Add;
fn add_two<Q, T>(v: Q) -> T
where
Q: Borrow<T>,
T: Add<Output = T> + Copy,
{
let rv = v.borrow();
*rv + *rv
}
fn main() {
let v1 = 5;
println!("{}", add_two(v1));
println!("{}", add_two(&v1));
})
While the owned call of add_two(v1) works pretty well, the borrowed call add_two(&v1) results in compiler error, complaining about that he can't infer T from Borrow<_>
error[E0283]: type annotations needed: cannot resolve `&i32: std::borrow::Borrow<_>`
--> src/main.rs:16:20
|
4 | fn add_two<Q, T>(v: Q) -> T
| -------
5 | where
6 | Q: Borrow<T>,
| --------- required by this bound in `add_two`
...
16 | println!("{}", add_two(&v1));
| ^^^^^^^
If I swap type parameters constrains for Q and T in where block the output would be different:
error[E0283]: type annotations needed: cannot resolve `_: std::ops::Add`
--> src/main.rs:16:20
|
4 | fn add_two<Q, T>(v: Q) -> T
| -------
5 | where
6 | T: Add<Output = T> + Copy,
| --------------- required by this bound in `add_two`
...
16 | println!("{}", add_two(&v1));
So what am I missing here? Why compiler can't resolve T from &i32 if Borrow has impl for both all T and all &T?
The problem is not that Q doesn't implement Borrow (because it does). The problem is this: how should the compiler know what T is? That is an output type parameter, and there can be several Borrow<T1>, Borrow<T2> etc. impls for a given type. Consequently, unless you tell the compiler what you want it to be (either by explicit annotations or by calling the function in a context where type inference can unambiguously deduce it), it can't assume anything.
So I actually have to produce two implementations by hand (or wrapped in macro) for ref and owned types?
BTW, while it isn't directly related to the previous question, but how can I impl trait both for all &T and all T, since compiler sees that as ambigious situation?
You don't have to produce two implementations. The current one is fine. Just slap an appropriate type annotation on the call when it's needed.
Well, it may work here, but I need it in My other topic about ref, owned, generics and iterator adaptor, where I want to create ParialSums trait should work for both Iterator<Item=T> and Iterator<Item=&T> and that whole thing confuses me a lot. For instance, I checked out how the std deals with it, for example sum and product, so std uses macro generated implementations, they in fact produce handcoded impl for each primitive numeric type AND for all references on them. And I can't find an apropriate way, how one can create his own collection of adapters easily on numeric types (and their references). Things that are easily done in haskell (never thought I would say easy and haskell in one sentence) are serious challenges for me in Rust.
You should then create an appropriate trait for all concrete types (numeric types and references to them), then use that trait bound to refer to them β don't try to distinguish between references and non-references at later points, contain repetition in the smallest place possible, at the earliest convenience.
For instance:
trait Numeric {}
impl Numeric for i32 {}
impl Numeric for u64 {}
impl Numeric for &'_ i32 {}
impl Numeric for &'_ u64 {}
trait PartialSum {}
impl<T: Numeric> PartialSum for T {}
By the way, your complaint against Rust and the appeal to the "easiness" of Haskell is unfair. Haskell doesn't provide ownership and reference semantics, so it doesn't let you do a whole bunch of things Rust can do (or at least not as efficiently), and it has to deal with fewer restrictions and corner cases as a result. Haskell also uses global type inference, which might be able to solve more, but Rust intentionally avoids it as in some cases (eg. function signatures, especially on API boundaries) it is beneficial to have type signatures, and letting everything to be inferred would be detrimental to readability.
Its OT, but interesting thing to discuss. I stopped tracking current Haskell state few years ago, but if I recall correctly, it's indeed a feature, but it's not an advised way for doing stuff. Explicit type signature is still a good and even a preferred practice in Haskell. I really like Haskell and I'm excited about learning Rust, while I miss beatuiful type hierarchy, provided by Haskell type classes in their stdlib, I really wish that we'll have some things like Number trait in Rust prelude too. My complaining isn't directly related to Rust, it's about what is considered a right way to do things in Rust, and I'm still trying to discover these ways in some aspects. I believe things would be more obscure when I'll look at async/await parts of Rust, I didn't even touched It yet, and quite interested to know what's the fuzz going on about It.
In this case, have a look at the rust-num project. This organization is a semi-official creator of all things numeric for Rust.
While I do agree that Rust is missing numeric traits in std (note that some of the rust-num traits/types will most probably be in std one day!), my general experience in several languages is that deep and complicated hierarchies don't help, they just make things messier. Interestingly, it's not really the Haskell libraries that demonstrate this failure mode. I experienced it a lot more in traditional OO languages (C++ and Swift comes to mind), where everything inherits from everything else and it's borderline impossible to get stuff done and navigate documentation because it's turtles, or rather, abstract superclasses all the way down.
For instance, trying to fit collections into a protocol hierarchy in Swift. It's just a massive pain without a real benefit. Never have I seen a use case where I didn't know if I needed a keyed or indexed, ordered or unordered, contiguous or tree-like container, and so I would have used the "nice" container protocols. If I wanted generic code, I'd find myself just falling back to Iterator (or whatever it's called these days β I stopped following Swift) anyway, for more generally useful/reusable code.