Strong type aliases/Type copy

Hello,

For safety and readability reason I tried to implement strong type aliases where the compiler would make a strong difference between the referenced type and the new alias.

type Alias =Reference;

... does not do the job since type names ca be substituted freely in function calls or assignments.

#[repr(transparent)]
pub struct Alias(pub Reference);

.... is functionally achieving this, but it sounds a bit like a hack and does not carryover all the associated functions and methods of the Reference type onto the Alias type.

Ideally I am searching for something that would change the behavior of the type checker only while LLVM would produce only one copy on the back-end for everything, to be used for both Alias and Reference.

Typical use case, is "Meters32 is alias of f32" and "Feet32 is alias of f32". Then you can statically check that there is no unintended feeding of feet in meters or vice versa. Such language feature can be handy in the critical software space, especially if there are no performance decrease in using it.

Many thanks

1 Like

This runs into similar issues as the more general "delegation" feature that has been discussed. How do you know which Self types to replace by the type alias? Once you start getting into more complex setups you actually want a variety of different new-types for different parts of operations:

type Meters = u32;
type MetersPerSecond = u32;
type Seconds = u32;

// For `Add` we can simply replace `Self => Meters`
impl Add<Meters> for Meters { type Output = Meters; }
// But for `Mul` that doesn't work,
// instead there's a variety of combinations possible
impl Mul<Seconds> for MetersPerSecond { type Output = Meters; }
4 Likes

I think you need to consider such aliasing atomically and in a non-reflexive manner (all associated functions and members are "copied" over from the Reference onto the Alias but the converse is not true).
Arguably I was thinking about Aliasing strings, but the proper to leverage on this on probably something like this:

pub trait PrimitiveTypeWithGroup : Add + Sub {}

#[repr(transparent)]
pub struct GroupElement<T:PrimitiveTypeWithGroup>(T);
impl<T:PrimitiveTypeWithGroup> GroupElement{
pub fn new(p : T)-> GroupElement{
GroupElement(p)
}

pub fn v(&self) -> T{
    self.0
}

}

impl<T:PrimitiveTypeWithGroup> Add<GroupElement> for GroupElement{
type Output = GroupElement;

fn add(self, rhs: T) -> Self::Output{
    GroupElement(self.0+rhs.o)
}

}

//....

type Meter32 = GroupElement;
type Second32 = GroupElement;
type MetersPerSecond32 = GroupElement;

impl Mul for Second32{ type Output = Meter32;

fn mul(self, rhs: MetersPerSecond32) -> Self::Output{
    Meter32::new(self.v()*rhs.v())
}

}

Then you can do the feet and hours versions where semantically correct.

Associated functions and methods aren't in any way special, they are just functions, like any other free function or method in your code. They just get nice syntax and name resolution.

So if you want to carry over all associated functions and methods to the wrapper, then you should also make it usable in all of other functions, at which point you just have a type alias.

The pattern in your p.2 is called "newtype". It's a pretty standard way to introduce different semantics for the same backing type. However, the point of making a newtype is to cast away some capabilities of the base type, including methods. A classic example is in @Nemo157 's comment: if you newtype u64 as Meters, you don't want to allow multiplying and dividing meters just because one can do the same thing for u64.

Put another way, any crate can add new methods to u64 by declaring and implementing a new trait. If newtypes worked as you want, this would mean that anyone can add arbitrary behaviour to your newtype, without knowing about it, breaking any invariants that you may try to uphold. Thus you would never really want to delegate everything, you just want a simple way to delegate some specific functionality that you use often. For that, there are various delegation crates which can automate some of the boilerplate. There is also a delegation RFC, but it's postponed, so don't expect that feature in the language in the foreseeable future.

P.S.: putting #[repr(transparent)] on your wrapper is also likely wrong. The point of that attribute is to be able to freely transmute between those types, which you likely neither need nor want. Overpromising on the APIs is a recipe for future trouble, because your requirements may change, so you have to change the implementation in a way no longer compatible with your copious API promises, causing all downstream code to break.

3 Likes

Thank you for the long answer.

I think you are missing my main point.

In particular this is not true:

So if you want to carry over all associated functions and methods to the wrapper, then you should > also make it usable in all of other functions, at which point you just have a type alias.

You may want to have the same operation on both types without the ability to cast them implicitly into one another (that is indeed, delegate everything).

Again I do not discuss a situation where new types would modify anything in the underlying type. If they were maintaining an invariant, those would be preserved as the referenced type does not change. A user could derive broken types by adding inconsistent constructs on those, but like any library user can create broken types.

I believe the picture is as follows:


                | Type keyword |  <my suggestion>  | newtype     
Implicit cast   |  Yes        |        No          |   No
Functions Avail |  Yes        |       Yes          |   No

Again the situation about multiplying meters and seconds is handled by first filtering out the proper math structure, but then reusing the math construct over and over across distinct, non fungible types though some kind of aliasing (delegate everything from the math structure filtered type).

Actually I believe I found something satisfactory here and there is no need to change anything: https://doc.rust-lang.org/1.26.2/unstable-book/language-features/repr-transparent.html

This is enough and will ease my work. Many thanks for the time.

The link to the unstable book you posted is stale. #[repr(transparent)] was stabilized a long time ago, and you have used it in OP. It also doesn't change the type safety rules in any way, so likely doesn't do what you think it does. It's purely for interfacing with foreign code via FFI.

Type aliases also don't have any implicit casts. A type alias really is its aliased type, in every way. I don't really understand what kind of feature you want, or rather I don't see any use case where automatically implementing all methods but keeping types somewhat distinct could be helpful.

It's also important for Rust-only unsafe code if you want to be able to make layout assumptions.

For example, if you want to offer &[Metres] → &[f32], then Metres needs to be a transparent newtype over f32 for that to be guaranteed sound.

2 Likes

It can be useful to have "ultra lightweight" newtypes. The point of such is solely to serve as a check on use; you can do anything with new type Meters = f32; you can with f32, because the singular and only point is to keep you from passing it where some API declares it takes some new type Feet = f32;. It's to introduce a noöp 0_f32 as Meters to attach a "tag" to the type.

If you allow impls on these types which don't apply to the underlying type, they also serve as a kind of way to do opt-in impl extensions.

Unfortunately, when working things through, you end up running into a lot of complications which make the approach not as useful as it seems on the surface. The big one is any signatures using Self; should these use the underlying type or the lifted type? It differs per impl item; e.g. you'd probably want Meters: Mul<f32> but Meters: Add<Meters>.

To address @Sylvain's OP, though, if what you want is such an ultra lightweight newtype, typically the way you'd do so is pub struct Meters(pub f32);, but only use the wrapper as a tag in interfaces, e.g. calling takes(Meters(len)) defined as fn takes(Meters(x): Meters).

This doesn't offer the type safety for locals, but it serves the purpose for tagging types at the API boundaries while adding the absolute minimum impl overhead.

7 Likes

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