Struct Alias

A struct alias would be a way to construct a new type from a previous one. Unlike type aliases, a struct alias is an owned type, so foreign traits can be implemented on it.

struct NewStruct = ForeignStruct; 

Rationale

Their main purpose is to make the new type pattern more ergonomic and involve less boilerplate. The new type pattern serves as a good solution to sidestepping orphan rules, but it is usually avoided for the amount of boilerplate that it involves.

This is not meant to cover every single possible use case that there is to the new type pattern, because that may not be at all possible. But rather cover the most frequent and often most painful use cases.

Inheritance and shadowing

The new type will inherit the entire public API of the old type, which includes fields, methods, associated constants, and trait implementations. Additionally, the new type has the possibility of shadowing methods and associated constants when new ones are defined.

Shadow vs override

To be clear, struct aliases cannot override methods or trait implementations in a way that can influence the original type. They can only expose a different API shadowing the inherited one. This conforms with the traditional new type pattern where the new type can expose a different API to the old one, but cannot tinker with the old's type traits and method implementations.

For example:

struct Parent { pub field: bool }; 
impl Parent {
    pub const NUM: i32 = 10; 
    pub fn get_num() -> i32 {
        Self::NUM
    }
}

struct Child = Parent; 
impl Child {
    //  shadows Parent::NUM
    //  Parent::NUM still exists, but it cannot be accessed
    //  through Child::NUM. It can still be accessed through `get_num()`
    pub const NUM: i32 = 99; 
}

let _ = Child { field: bool }; // you can construct a Child just like a Parent. 
assert_eq!(Child::NUM, 99); // Child::NUM shadows Parent::NUM
assert_eq!(Child::get_num(), 10); // note that this still returns Parent::NUM

Inherited traits can also be shadowed by implementing them over the type.

#[derive(Debug)]
struct Parent;
impl Parent {
    fn print() {
        println!("{Parent}"); 
    }
}

struct Child = Parent; 
impl Debug for Child {
    fn fmt(&self, f: Formatter<'_>) -> fmt::Result<()> {
        write!(f, "Child"); 
    }
}

Child.print() // still prints "Parent"
println!("{}", Child); // prints "Child"

Casts

All struct aliases are types in their own right, so they do not support implicit coercion. In order to cast one to another users must manually implement From or use a derive macro.

Struct aliases are guaranteed to have the same layout, so it is always safe to transmute one type into the other. They work as if the New type was defined with repr(transparent). This guarantee is important, because if the foreign type has private fields, users will not be able to cast one type into the other.

// a From derive macro would expand to this.
let new = unsafe { transmute::<ForeignType, New>(foreign) }; 

Privacy

The privacy for all fields of a struct alias is set to be at most pub(crate). This ensures that refactoring a struct alias into a proper struct is never a breaking change.

// its ok to change this: 
pub struct New = Old; 
// into this, because library users weren't able to access the fields of `New`
pub struct New(Old); 
1 Like

So this means there's no[1] difference between struct aliases and delegation[2], right?

But it's terribly impractical to actually have it be nonbreaking, though, as you'd have to delegate all of the lost inherent items and trait implementations.

You forgot the #[repr(transparent)] you're providing :slightly_smiling_face:


While this is indeed useful, the resulting behavior is almost the same as #[derive(Deref, DerefMut)] and should be justified how it is meaningfully better than that.

I suspect the answer is having the struct alias prevent using &[mut] NewStruct where &[mut] ForeignStruct is expected is desired and thus the answer (especially since coercing to &[mut] ForeignStruct would use the foreign impls rather than the shadowed ones), but this should be spelled out.

Additionally, I'd say the fields should be publicly accessible (as with Deref[Mut], but maybe hopefully still allowing mut splitting?) and if field hiding is desired to use a tuple struct and delegation from the beginning.


  1. well, rather the difference is just a. automatic delegation of every trait and inherent item and b. removal (shadowing) of delegation when directly provided ↩︎

  2. obviously a built-in has the difference to ambassador's library implementation of working for any type (and inherent impls) and not just annotated traits in addition to the above difference to built-in delegation ↩︎

1 Like

This differs from delegation in that here the new type does actually implement all traits the old type implements, as opposed to using autoderef to behave as if it implements them. In addition, it would inherit all the previous types constructors and associated functions/constants. In contrast, delegation can only inherit methods.

Now that you mention it, changing from one to the other may be non breaking if you implement Deref and DerefMut.

why not use const generics instead?

struct ParentAndChild<const N:i32> {pub field:bool}
impl<const N:i32> ParentAndChild<N>{
    pub const NUM:i32 = N;
    pub fn get_num()->i32{
        N
    }
    fn show(&self)->i32{
        N
    }
}
type Parent=ParentAndChild<10>;
type Child=ParentAndChild<99>;
fn main(){
    let a=ParentAndChild::<12>{field:true};
    println!("{}",a.show());
    println!("{}",Parent::get_num());
    println!("{}",Child::get_num())
}

Note that by delegation I do not mean #[derive(Deref[Mut])]. I mean the theoretical Rust feature that would let you write something like

struct Child(Parent);
impl Child {
    pub use {self.0}::get_num;
}
impl Debug for Child {
    pub use {self.0}::fmt;
}

Consider

struct NewU32 = u32;

let a: NewU32;
let b: NewU32;

Are these expressions valid, and if so, what is their type?

  • a + b
  • a.checked_add(b)
  • a.overflowing_add(b)
  • NewU32::MIN
  • a.trailing_ones()
  • a << b
11 Likes

u32 is not a struct, so I guess this would not be allowed.

However, you make a good point that automatically deriving traits can be problematic. In the case of Add<Rhs>, Rhs defaults to Self. So in this example:

struct Foo(u32);

impl Add for Foo {
    type Output = Foo;
    ...
}

struct Bar = Foo;

Bar would implement Add<Bar, Output = Foo>, but neither Add<Foo, Output = Foo>, nor Add<Bar, Output = Bar>.

This breaks the assumption that Self in an impl block can be replaced with the type, and vice versa.

Where is it said that the wrapped type needs to be a struct? From what I understand the struct keyword is there to say that the wrapper is a struct (e.g. struct NewU32 = u32; becomes struct NewU32(u32); plus all the implementations)

All of this is correct if you assume that Self will remain Self when extending traits, meaning impl Add for Foo actually implements Add<Self> for Foo. There's also another way to see it, that is Self gets resolved in the implementation, so impl Add for Foo implements Add<Foo> for Foo, and any subsequent use of that implementation won't know of the Self.

This is still problematic though, as now the wrapped type gets leaked everywhere a Self was implied.

Oh, I wasn't familiarized with that feature. In that case, it may not be that much different.

All appearances of Self are automatically changed to NewU32, so all previous constructors, operators and functions return NewU32, and all methods take NewU32.

I think @SkiFire13 chose an interesting set of examples, and this answer that says little more than “everything works” doesn’t really address these examples properly at all.

For example u3::checked_add does not return Self = u32, but instead Option<u32>. How would this be handled and would that way of handling it make sense? Operators like + operate via traits. So u32: Add<u32> is a trait implementation. Transferring all trait implementations could mean NewU32: Add<u32>, whereas replacing all instances of Self could mean NewU32: Add<NewU32>. Or would we want both?

u32::trailing_ones returns u32, but the same operation on other types (e.g. u8::trailing_ones or i64::trailing_ones returns u32, too!) Always blindly replacing matching output types would mean that a NewU32::trailing_ones returns NewU32 while a NewU8::trailing_ones would still return u32?

<< is implemented for very many combinations of types, e.g. x: u8, y: isize support x << y and y << x, etc… Can NewU32 interact with all those types and NewU32 itself but not u32? Or something else? What exactly, and why, and is that a good idea for a general rule?

7 Likes

I'm trying to wrap my mind around this, so I'm thinking out loud here... please correct me if I make any mistakes.

This feels like syntactic sugar for creating a newtype, and I mean that in the most literal sense. Basically, the following happens:

  1. When the compiler hits struct NewStruct = ForeignStruct, it copies the source code for ForeignStruct into the current file (kind of like macro expansion). This could be a recursive copy if there are sealed traits or other bits that have to be copied over.
  2. The compiler does something almost like sed s/ForeignStruct/NewStruct/g across the newly copied source code.
  3. The compiler continues processing code that was in the NewStruct file, using new implementations to replace the code for old implementations.

Is this essentially what you're proposing?

Oh, here's a couple of other interesting examples:


struct NewString = String;
let s: NewString;

What's the type of s.replace("foo", "bar")? Note that String, and thus NewString, doesn't have a replace method, it actually comes from dereferencing to str! Thus it would use the normal str method and return String.


You might also want to have a struct NewStr = str;, but how do you change all NewString's methods that use &str to use NewStr?


trait A {}
trait B {}
trait C {}

struct Foo<T>;
impl<T: B> A for Foo<T> {}

struct Bar<T> = Foo<T>;
impl<T: C> A for Bar<T> {}

Is A implemented for Bar<T> where T: B + !C?

  • if yes, isn't this specialization, with all its soundness problems?
  • if no, this is kinda surprising, it means you can lose traits implementations (well, technically you already do with the ones where Self is replaced by the wrapper struct).
6 Likes

This adds a semantic difference to an impl using the Self alias or just saying T which currently doesn't exist. For the reasons @steffahn mentioned just swapping all mentions of the self type which don't use Self is also problematic. And as SkiFire points out you can have indirect mentions of the self type as well.

Additionally, this is an implementation concern, as Self (and other name aliases) are eagerly normalized in the compiler, so it's currently impossible to (reliably) tell if a transparent alias was used.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.