Pre-RFC: Auto-dereferencing parameters passed to generic functions as well as concrete ones


#1

When migrating some of my functions to generic functions, I came across a few inconsistency errors. Mainly, when passing an &thing to a concrete function, it is auto-dereferenced, but when passing it to a generic function, it isn’t.

Here’s an example of what I mean:

/// A dummy trait requirement
trait TraitRequirement {
    fn get_some_int(&self) -> u8;
}

/// Some struct which implements the required trait
struct SomeStruct {
    inner_int: u8,
}
impl TraitRequirement for SomeStruct {
    fn get_some_int(&self) -> u8 {
        self.inner_int
    }
}

/// Dummy function which takes anything that implements TraitRequirement
fn some_function_generic<T: TraitRequirement>(thing: &T) {
    println!("Result: {}", thing.get_some_int())
}

/// Dummy function which requires SomeStruct specifically
fn some_function(thing: &SomeStruct) {
    println!("Result: {}", thing.get_some_int())
}

fn main() {
    // Let's say we got this from somewhere else, this is just an example.
    let boxed_struct = Box::new(SomeStruct {
        inner_int: 5u8
    });

    // This works
    some_function(&boxed_struct);
    
    // This does not work
    some_function_generic(&boxed_struct);
    
    // This works, but is confusing as there is no &* needed when using a concrete function
    some_function_generic(&*boxed_struct);
}

Playpen link: http://is.gd/M3Zbto

The fact that some_function(&outer) works, but some_function_generic(&outer) doesn’t would be pretty confusing to someone who is just being introduced to generics. It also makes changing a concrete function into a generic one a breaking change.

I’m proposing that we get rustc to “try” dereferencing any types passed to a generic function to find one that fits the trait before throwing an error. In the example above, before having an error in the second function, it would try dereferencing it once to just SomeStruct, and since that does fit the trait, it wouldn’t error.

P.S. The examples/content I used in the original post were for the same purpose, but were put quite a bit differently. I suggest looking at the post history if you are confused by the initial replies.


String type coercion in rust
#2

This is current behavior is technically correct, but I don’t think it’s really intuitive. T and &T are not the same types, but I have a feeling that many people tend to think about them as if they were. There is actually some automatic dereferencing in action for things implementing the Deref trait, but I don’t think there is any dereferencing done for Borrow. Anyway, the referencing does still apply for generic types, so you should probably ask for &T in this case. Here are some alternative implementations of func:

fn func<T: Borrow<str>>(a: &T) {}
fn func<T: Deref<Target=str>>(a: &T) {}
fn func(a: &str) {}

Yes, the last one should work as long as the input can be dereferenced to str :wink:


#3

@ogeon Even though it is correct at this point, I do find it unintuitive that &T sometimes auto-dereferences, but not all the time. Because &String dereferences to &str automatically, I think it’s a bit unintuitive that a function using typed parameter doesn’t.

I realize Borrow<str> is a bit of a bad example, as &str works just as well - I’ll try to think of a better example for this


#4

@daboross I think I understand the problem and I wasn’t arguing against it. There can sometimes be a slight difference between the object thought model and the actual data representation and I think this case illustrates part of this difference. It’s common to think about &T and T as just something of the type T, and that’s also kind of the case in Java (but different representation). The problem with that model is that &T and T can no longer be different even though they may have to. Silly example: T may be thread safe, but not &T.

The seemingly arbitrary ability to being automatically dereferences is not as arbitrary as it seems. Tings implementing Deref are essentially smart pointers, like Box and Rc, and pointers/references are supposed dereferences. Borrow<T> is more useful to say that it’s possible to borrow T from the type implementing it it can be implemented for several types T. So, to summarize, values are always dereferenced as long as they are declared as references by implementing Deref.


#5

I think I understand &Type vs Type, but what I’m trying to ask about is the difference in automatic dereferencing. I realize you need to implement Deref<T>, but I don’t understand why this works for the exact same struct when using a concrete function but it work doesn’t with a generic one.

Let me show you some more fleshed out examples. Using Borrow<str> and &String was truly a bad example.

I have two examples, one using a custom implementation of Deref, and the other just showing it using a Box<SomeStruct>. The problem is shown via comments in the main() function at the end of each one.

Example 1, manually implement Deref for a type:

use std::ops::Deref;

trait TraitRequirement {
    fn get_some_int(&self) -> u8;
}

/// InnerStruct is the inner thing which implements the required trait
struct InnerStruct {
    inner_int: u8,
}
impl TraitRequirement for InnerStruct {
    fn get_some_int(&self) -> u8 {
        self.inner_int
    }
}

/// OuterStruct doesn't implement the required trait, but it does dereference to InnerStruct
struct OuterStruct {
    inner: InnerStruct,
}
impl Deref for OuterStruct {
    type Target = InnerStruct;
    fn deref(&self) -> &InnerStruct {
        &self.inner
    }
}

// Dummy function which requires something that implements TraitRequirement.
fn some_function_generic<T: TraitRequirement>(thing: &T) {
    println!("Result: {}", thing.get_some_int())
}

// Dummy function which requires InnerStruct specifically
fn some_function(thing: &InnerStruct) {
    println!("Result: {}", thing.get_some_int())
}

fn main() {
    let outer = OuterStruct {
        inner: InnerStruct {
            inner_int: 5u8
        }
    };

    // This works
    some_function(&outer);
    
    // This does not work
    some_function_generic(&outer);
    
    // This works, but is confusing as there is no &* needed when not using a generic function
    some_function_generic(&*outer);
}

Example 2, just showing it with Box<SomeStruct>

trait TraitRequirement {
    fn get_some_int(&self) -> u8;
}

/// Some struct which implements the required trait
struct SomeStruct {
    inner_int: u8,
}
impl TraitRequirement for SomeStruct {
    fn get_some_int(&self) -> u8 {
        self.inner_int
    }
}

// Dummy function which requires something that implements TraitRequirement.
fn some_function_generic<T: TraitRequirement>(thing: &T) {
    println!("Result: {}", thing.get_some_int())
}

// Dummy function which requires SomeStruct specifically
fn some_function(thing: &SomeStruct) {
    println!("Result: {}", thing.get_some_int())
}

fn main() {
    let boxed_struct = Box::new(SomeStruct {
        inner_int: 5u8
    });

    // This works
    some_function(&boxed_struct);
    
    // This does not work
    some_function_generic(&boxed_struct);
    
    // This works, but is confusing as there is no &* needed when not using a generic function
    some_function_generic(&*boxed_struct);
}

The thing that is confusing me is that some_function_generic(&outer) doesn’t work, while some_function(&outer) does. The auto-dereferencing works for one, but not for the other.


#6

Ah, I get it now. I’m so sorry for missing the point and thanks for your patience with me :confounded: . I’m actually surprised by this, as I thought it worked in the same way as with concrete types and when a method from a contained type is called. This feels like a really bad inconsistency and I wish it worked. I’m interested in hearing a motivation for this if there is any.

I wonder why I never bumped into this problem before… Maybe I did without realizing it.


#7

No problem at all! I understand where you were coming from with how I was asking it before.

One other thing this causes is that it makes moving from a concrete function to a generic function a breaking change. If something to fix this was implemented, I think that moving from a concrete to a generic function would no longer be a breaking change.


#8

Sounds reasonable, given that the bounds on the generic type would include the previously required concrete type.


#9

In principle, it seems like it should work, but consider the following:

http://is.gd/493vzc

use std::ops::Deref;

struct A(TargA);
struct TargA;

impl Deref for A {
    type Target = TargA;
    fn deref(&self) -> &TargA { &self.0 }
}

struct B(TargB);
struct TargB;

impl Deref for B {
    type Target = TargB;
    fn deref(&self) -> &TargB { &self.0 }
}

struct C;

trait Requirement {
    type Required;
}

impl Requirement for B {
    type Required = C;
}

impl Requirement for TargB {
    type Required = A;
}

fn check_requirement<T: Requirement>(_r: &T::Required, _t: &T) {}

fn main(){
    check_requirement(&A(TargA), &B(TargB));
}

How do you propose for the compiler to handle this case? The types in the call to check_requirement will only be resolved if the second argument—and not the first—is dereferenced. However, in order to discover this in the case where there are n arguments with m levels of indirection each, the compiler would have to search mn possible combinations!

Edit: Not to mention that more than one of those could be valid.


#10

That’s a good point. I imagine that it could escalate far too quickly to be worth it, unless some mechanism is introduced to stop it if it will take too much time, if possible.


#11

I guess that really would stop this from working, as the inconsistency of not handling that case would be worse than the inconsistency of not auto dereferencing for generics at all. Thank you for pointing this out!