Possibly unsized type arguments to type aliases

Continuing the discussion from Type constructor to hide implementation details of typestate builder:

Apparently type arguments to a type alias are implicitly ?Sized:

use std::borrow::Cow;

type Foo<T> = Cow<'static, T>;

pub fn foo() -> Foo<str> {
    Cow::Borrowed("Hello")
}

(Playground)

I even get a warning if I add a bound T: ?Sized:

-type Foo<T> = Cow<'static, T>;
+type Foo<T: ?Sized> = Cow<'static, T>;

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
warning: bounds on generic parameters are not enforced in type aliases
 --> src/lib.rs:3:13
  |
3 | type Foo<T: ?Sized> = Cow<'static, T>;
  |             ^^^^^^
  |
  = note: `#[warn(type_alias_bounds)]` on by default
help: the bound will not be checked when the type alias is used, and should be removed
  |
3 - type Foo<T: ?Sized> = Cow<'static, T>;
3 + type Foo<T> = Cow<'static, T>;
  |

warning: `playground` (lib) generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.76s

What should I do if I want T to include unsized types? Should I just omit the ?Sized bound like in the first Playground?

Even implicit bounds like Sized aren't checked, so yeah, just omit the ?Sized bound.

Example.

1 Like

I feel like the reference doesn't reflect this properly:

Sized

The Sized trait indicates that the size of this type is known at compile-time; that is, it's not a dynamically sized type. Type parameters are Sized by default, as are associated types. Sized is always implemented automatically by the compiler, not by implementation items. These implicit Sized bounds may be relaxed by using the special ?Sized bound.

And it isn't clarified here either:

Type aliases

[…]

A type alias, when not used as an associated type, must include a Type and may not include TypeParamBounds.

A type alias, when used as an associated type in a trait, must not include a Type specification but may include TypeParamBounds.

A type alias, when used as an associated type in a trait impl, must include a Type specification and may not include TypeParamBounds.

Where clauses before the equals sign on a type alias in a trait impl (like type TypeAlias<T> where T: Foo = Bar<T>) are deprecated. Where clauses after the equals sign (like type TypeAlias<T> = Bar<T> where T: Foo) are preferred.

I do not see any note here saying that the type parameters are not Sized, or did I miss something? It only states that it cannot include bounds (in the syntax) unless used in a trait. Should the reference be improved in that matter, or did I miss something?

The reference should be improved.

There's other undocumented (and weird) alias behavior too.

It doesn't say "in the syntax", it just says there can be no bounds. I agree that the documentation is ambiguous, but one can reaonably read it as "type aliases never have any bounds on their parameters". Which, of course, includes any possible default T: 'static or T: Sized bounds.

In other words, a type alias isn't an abstraction boundary. It's just a readable name for the term of the corresponding type. This interpretation, unfortunately, is not quite compatible with the proposed for stabilization impl Trait in type aliases.

We do seem to be headed straight for a real mess between all of

  • type aliases that kinda-sorta ignore bounds except when they don't
  • TAIT
  • GATs that take bounds and have not-always-sensible defaults
  • non-G ATs that don't take bounds (still? haven't checked) and don't have defaults

Not sure how much we can get away with fixing though.

4 Likes

While they do use essentially the same syntax, so comparing their behavior is fair, do note that type aliases and associated types are distinct concepts.

From a usage behavior perspective, type alias impl trait is probably closer to an associated type. Ignoring how it gets inferred, you could potentially think of

// opaque type alias
type Tait<'a, T, U> = impl Trait;

as acting similar to

// type projection
trait TaitImpl<'a, T, U> {
    type Resolved: Trait;
}
// transparent type alias
type Tait<'a, T, U> = <() as TaitImpl<'a, T, U>>::Resolved;

except still generic over TaitImpl such that you can't use the concrete resolved type, although I'm certain there's details about how lifetime/generics capture works that differ slightly from this simple expansion.

But yeah, there's definitely some unfortunate differences between different places and ways you can write type X =. Personally, I'm still hoping inherent associated types happen eventually. But to the specific point about GATs, the where Self: 'a bound AIUI is currently required such that T-lang can figure out what the consistent behavior of generic and nongeneric associated types should be and hopefully use that in the future.

1 Like

It doesn't say it literally, but "TypeParamBounds" refers to the section above, which is having a heading "syntax":

Syntax

TypeAlias :
type IDENTIFIER GenericParams? ( : TypeParamBounds )? WhereClause? ( = Type WhereClause?)? ;

I think it's not a big deal, but it did surprise me a bit, particularly because the error message didn't say that ?Sized was unnecessary but warned me the bound would not be "checked":

help: the bound will not be checked when the type alias is used, and should be removed

A ?Sized bound isn't really a bound but a relaxation of a default bound. Thus I wasn't entirely sure if I would run into problems with !Sized type parameters (though testing it on Playground revealed that it isn't a problem, even if not described explicitly in the reference).

There is no default Sized bound in general. Sized is just a trait, so the only absolute default is "it's not implemented". However, specifically for generic parameters on ADTs and functions there is a default Sized bound, for ergonomic reasons.

Which is the majority of uses in practice, of course, so it does feel like Sized is some absolute default, but it isn't.

The reference says here:

And if I write type A<T> = B<T>;, I would assume that T is a type parameter here.

1 Like