Why access subtyping is not allowed in type extensions?

Hi, i was messing with rust for a bit and encountered a behavior that can be best described as a type system restriction. Here goes the code:

trait HuntingSkills {}
struct Animal {}
impl HuntingSkills for mut Animal {}
//Unfortunately, compiler halts with error message. 

This bring some questions about semantics of types being not having a distinction of mutability and immutability in type system, but rather as a constraint on variables. But what if I needed to define a new functionality to only mutable version of a type, because otherwise extending just supertype would cause a violation of information hiding?

trait TrA { field: Type }
struct A {}
impl TrA for A { ... }

Now passing A to a function as immutable or mutable argument exposes field member to a caller, which might be undesired behavior. Also note that this is different to access scope constraints, since i want a type to be available for foreign code, but to only those functions, which are taking this type as immutable(or mutable whatever) one.

What are your thought about this?

All values are mutable, this is why Vec::<()>::new().pop() works. and there is no such things as mut Animal. let mut ... is a property of the bindings, not of the types (we could remove let mut and not change the semantics of Rust)

1 Like

That is the point :flushed: please google structural and nominal typing and subtyping

I know the difference, I don't see how that's relevant here though. Traits can't have fields so your second example doesn't make sense (unless you are proposing an extension to Rust). Currently, if you wanted to access a field you would need to use a function like fn field(&self) -> &FieldType. Of course this runs into issues with the borrow checker because it can't see disjoint borrows across function boundaries.

(this issue can be alleviated with some sort of partial borrows)

Partial borrows have a long history, going back to 2016 on these forums

2 Likes

What i propose is gonna solve this :flushed:

To be completely honest, I'm not sure what you're asking for.

It should be noted that mutability of owned values is not part of the type system, it's a property of bindings. In fact, you can go from a non-mut binding to a mut one:

let list = Vec::new();
// list.push(0); // compile error
let mut list = list;
list.push(1); // just fine

If you want a method to only be available on a mutable binding, you can either have it take &mut self, or you can implement directly on a mut reference:

impl HuntingSkills for &'_ mut Animal { ... }
3 Likes

Oh my, so it is possible to extend mutable reference subtype, but not just mutable type? That's just unsound, imo :grimacing:

How? Can you please elaborate? The defining characteristic of &mut T is not mutability, but uniqueness. So I don't see how the absence of mutable types is a soundness issue. It looks like you have some misunderstandings about how Rust operates.

5 Likes

Like many other design decisions, this makes more sense when you realize that "mutable reference" is just a convenient oversimplification that we use to make the life of beginners easier. See also:

Owned Rust values can always be mutated, just like computer DRAM can. But as soon as pointers come into play, you need to ensure that you have unique access to a value in order to safely perform some forms of access, including basic mutation without extra precautions. Hence &mut T, the unique reference type, being the key to mutation by default.

From the perspective, it is mut bindings that are the odd ones. It's always safe to mutate a value from a type system and language soundness perspective, so from the language's perspective there is no need for a mut T. The only reason why we distinguish mutable and immutable bindings is as an ergonomic aid to help the program's reader figure out where mutation is ongoing.

2 Likes

I have tried this code and it confirmed my initial thought about subtyping relation.

trait HuntingSkills { fn test(&mut self); }
struct Animal {}
impl HuntingSkills for &mut Animal { fn test(&mut self) {} }
//wat?! &mut T is alowed but not others?
//impl &mut Animal {} fails!!


fn main() {
    let var1 = Animal {};
    //var1.test(); knows var1 is imut T
    let mut var2 = Animal {};
    //var2.test(); knows var2 is mut T
    let var4 = &var1;
    //var4.test(); //error compiler does distingusih between T and &T
    let mut var3 = &mut var2;
    var3.test(); //this works since explicit impl was defined for &mut T (Animal)
    println!("reached");
}

Meaning there is a flaw is lang's design in that it cannot address other sybtypes, which are:

  • T (used as type declaration and supertype for all subtypes)
  • mut T
  • &T
  • &mut T
  • dyn U
  • dyn mut U
  • dyn & U
  • dyn &mut U

So all these subtypes should be addresable with impl derictive. The most obviuos outcome is that you may lift nessecity for explicit self in methods

impl T { fn test(&mut self) {} } //old style
impl mut T { fn test() {} } //with subtyping

I think this is actually where the confusion comes from (playground):

use std::sync::atomic::{AtomicU32, Ordering};

struct S(u32);

impl S {
    fn shared(&mut self) {}
}

fn main() {
    let var1 = S(5u32);
    // ERROR: cannot assign [...] `var1` is not declared as mutable:
    //var1.0 = 10u32;  
    // ERROR: cannot borrow as mutable [...] `var1` is not declared as mutable:
    //var1.shared();   
    
    // requires 'mut':
    let mut var2 = S(5u32);
    var2.0 = 10u32;       // fine now
    var2.shared();        // fine now

    // === Atomics counter-example ===
    let a = AtomicU32::new(32u32);      // Look Ma, no 'mut'
    a.store(64u32, Ordering::Relaxed);  // this is still fine
    
    println!("reached");    
}
1 Like

I think the very concept of "subtypes" in Rust is what trips you up. There are no subtypes in Rust.

There's something that looks a lot like it if you look at supertraits, and there are lifetime relations (which IIRC are modeled internally as subtypes in rustc), and there are some attempts at abstracting from &T and &mut T but none of these are real subtype relations.

4 Likes

I think what OP really wants is akin to function overloading based on mutability. Rust doesn't do that, either, hence whey there is the fn foo(&self) and fn foo_mut(&mut self) convention.

2 Likes

Indeed. But what is really needed then is a way to properly abstract over &T and &mut T, and perhaps while we're at it over Box<T>, Rc<T> and Arc<T> as well. That would be more fundamental (and thus more widely applicable) than merely being able to overload methods based on mutability of the receiver.

In addition, I'm not sure that overloading based on receiver is even feasible as such - overloading implies multiple methods with the same symbol. As long as Rust aims to be interoperable with C, how far can it go in extending symbol lists before breaking that compatibility?

These impls all work:

impl Trait for Struct {}
impl Trait for &Struct {}
impl Trait for &mut Struct {}

impl Trait for &dyn Trait {}
impl Trait for &mut dyn Trait {}

A direct impl on a reference type is disallowed because the reference type is defined in core, not in your crate.

And, may I reiterate:

  • None of these things are subtypes.
  • mut T is not a thing. mut is a quality of a binding or a reference, not the T itself.
  • dyn & U isn't a thing; the order is &dyn U.
    • This really makes me think you haven't even tried it.

Further:

impl mut T { fn test() {} }

Even if this were allowed, it would not be called with method syntax. Because it doesn't take a self parameter, it is what other languages would call a static method; a function of the type, not the instance.

<mut T>::test()

To completely frank: it seems like you've fundamentally misunderstood Rust's type system, and how method selection works at a fundamental level. I'd suggest going over to users.rust-lang.org for language usage questions, or the community Discord, in order to familiarize yourself with the language alongside the book.

12 Likes

False, Rust has subtypes in lifetime dependent type, i.e. &'a T is a subtype of &'b T, of 'a: 'b. Otherwise, yes Rust doesn't have subtypes.

2 Likes

I named that situation explicitly: lifetime relations. They are modeled as subtypes internally, but good luck exploiting that fact in Rust-the-language other than purely for making sense of things. It's not possible to do any data modeling in the way you would in Java, for example.

So I stand by what I said.

3 Likes

How, if you have read this,

let var1 = Animal {};
    //var1.test(); is error
let mut var2 = Animal {};
    //var2.test(); is error
let mut var3 = &mut var2;
    var3.test(); //ok

you dont see there subtyping relation?

What type type, is so, than let mut a = Animal{}; has? Given that in [Why access subtyping is not allowed in type extensions? - #10 by jele] this example it is not possible to access test() in let mut a = Animal{};, but is possible to do so in let mut b = &mut a;? Type and reference to it are different things in term of type relations.

This doesnt. impl & Animal, impl &mut Animal,

I did

And what is the point here?

Because there isn't one. You only have two types at play, Animal (var1 and var2) and &mut Animal (var3). The fact that some of the bindings are mutable is irrelevant. I will repeat, let mut ... can be entirely removed from Rust without changing Rust in any fundamental way.

let mut a = Animal; has type () because all "statements" have type (). \s

I guess what you meant to ask was "what is the type of a", and that would be Animal. That mut is nothing but a lint.

Yes, a type T is different from a reference to the same type (&mut T). These references are what form the backbone of Rust's safety. Specifically, &mut T being a unique reference, and &T being a shared, immutable by default, reference.

This doesn't work because you didn't define the reference types, not because of anything related to sub-typing.

Given that some of your examples don't compile due to syntax errors is suspicious. i.e. dyn & U is invalid syntax.

Sub-typing is irrelevant for functions without receivers.

2 Likes