Why does the following code compile [multi-parameter traits]:

Apologies if this has been discussed here before, I wasn't quite sure how to search for this question, since I'm not sure Rust calls this a "multi-parameter" trait.

trait MyInto<Output> {
    fn my_into(&self) -> Output;
}

impl MyInto<i8> for String {
    fn my_into(&self) -> i8 {
        2
    }
}

fn main() {
    println!("{:?}", "foo".to_string().my_into())
}

(Playground)

As far as I can tell, it's impossible to get the same code in Haskell to compile; you would need to specify the return type. It seems that in Rust, the compiler decides there's only one instance to use, and therefore it can use it? If I add another instance for String, say for i32, the compiler fails with

cannot infer type for `Output`

You might use an associated type to enforce one and only one Output type per trait instance (in Haskell, you can use functional dependencies in the same way, and the compiler will no longer need type annotations).

I'm curious whether there are any downsides to the compiler infering an instance that isn't guaranteed to be the only instance that applies? One that I can think of is that adding an instance can cause failures in other places in your code. Also, is there a benefit beyond just not needing to write a type annotation that seems obvious?

Thanks.

1 Like

I think it's called generic trait.

You can implement as many MyInto<Output> for String as you want, the compiler just isn't sure which one to use. playground

It will add some work for the compiler but your compiled code will not suffer from it. The compiler will replace every methods with the right one as long as you don't use trait object. In this case it's dynamically dispatched but having a generic parameter doesn't add any cost to it.

It depends on what you want to do, if a type should only have one Output but it's not the same for every type, use an associated type. If a type can have multiple Output, use a generic parameter like you did.

For example std::convert::From has a generic parameter, whereas std::iter::Iterator has an associated type.

1 Like

This is confusing. The compiler guarantees there is only one that applies or as you show give an error.

A benefit of the inference is that you can use the generic function inside another.

I meant that there is no guarantee in the type system that there is only one instance. So it's more about how the code might change in the future.

I believe this means that any new instance for a generic trait is a breaking change, because any code that uses the trait might implicitly rely on there being only one instance.

1 Like

Breaking inference is fine under Rust's stability guarantees. There are many ways to break inference besides what you showed here, for example, adding a public item to your api can break inference due to glob imports. If breaking inference was a breaking change we would get nothing done.

I just tested it. The same problem also appears across crate boundaries.

frunk heavily (ab)uses this to dodge overlapping impls on its HLists and Coproducts:

#[macro_use] extern crate frunk;

let h = hlist![9000, "joe", 41f32, true];

// This resolves to h.get::<f32, There(There(Here))>.
//
// The second type parameter is necessary to avoid conflict between the
// base case impl and recursive case impl. Because rust performs reasoning
// based on the uniqueness of type parameters in existing impls, the correct
// type of There(There(Here)) can be silently computed by type inference
// without bothering the user.
let x: &f32 = h.get();
2 Likes

that's very interesting! and if we use two f32 types, we get the expected error:

let h = hlist![9000, "joe", 41f32, true, 31f32];
let x: f32 = *h.get();
error[E0282]: type annotations needed
  --> src/main.rs:51:21
   |
51 |     let x: f32 = *h.get();
   |                     ^^^ cannot infer type for `TailIndex`