Default Type Parameters on Traits

Status: Approved Approved: 2026-01-31 Author: Claude Created: 2026-01-31 Depends On: None Enables: operator-traits-proposal.md (with default-associated-types-proposal.md) Parallel With: default-associated-types-proposal.md

Summary

Allow type parameters on traits to have default values, enabling trait Add<Rhs = Self> where Rhs defaults to Self if not specified.

Motivation

Without default type parameters, every impl must specify all type arguments:

// Today: verbose
impl Vector2: Add<Vector2> { ... }
impl Vector2: Eq<Vector2> { ... }

// Goal: concise when types match
impl Vector2: Add { ... }  // Rhs defaults to Self = Vector2
impl Vector2: Eq { ... }   // Rhs defaults to Self = Vector2

This is especially important for operator traits where the common case is operating on the same type.

Design

Syntax

Default type parameters use = Type after the parameter name:

trait Add<Rhs = Self> {
    type Output
    @add (self, rhs: Rhs) -> Self.Output
}

trait Convert<Target = Self> {
    @convert (self) -> Target
}

Semantics

  1. Default applies when impl omits the type argument
  2. Self in default position refers to the implementing type
  3. Defaults are evaluated at impl site, not trait definition site
  4. Parameters with defaults must appear after all parameters without defaults
trait Example<T = int, U = T> {
    @method (self, t: T, u: U) -> void
}

// These are equivalent:
impl Foo: Example { ... }
impl Foo: Example<int, int> { ... }

// Partial specification:
impl Bar: Example<str> { ... }  // U defaults to T = str
impl Bar: Example<str, str> { ... }  // equivalent

Ordering constraint example:

// Valid: default after non-default
trait Transform<Input, Output = Input> { ... }

// Invalid: non-default after default
trait Invalid<T = int, U> { ... }  // Error: non-default parameter after default

Self Resolution

Self may be used as a default value in trait type parameters. It refers to the implementing type at the impl site.

trait Add<Rhs = Self> { ... }

impl Point: Add { ... }
// Rhs = Self = Point

impl Vector2: Add { ... }
// Rhs = Self = Vector2

Grammar Change

Current:

type_param = identifier [ ":" bounds ] .

Proposed:

type_param = identifier [ ":" bounds ] [ "=" type ] .

Multiple Defaults

Later parameters can reference earlier ones:

trait Transform<Input = Self, Output = Input> {
    @transform (self, input: Input) -> Output
}

impl Parser: Transform { ... }
// Input = Self = Parser, Output = Input = Parser

impl Parser: Transform<str> { ... }
// Input = str, Output = Input = str

impl Parser: Transform<str, Ast> { ... }
// Input = str, Output = Ast

Implementation

Type Checker Changes

  1. Parse default type in type_param grammar rule
  2. When checking impl Type: Trait:
    • Count provided type arguments
    • Fill missing arguments with defaults
    • Substitute Self with implementing type
    • Proceed with normal impl checking

Example Resolution

trait Add<Rhs = Self> {
    @add (self, rhs: Rhs) -> Self
}

impl Point: Add {
    @add (self, rhs: Point) -> Self = ...
}

Resolution steps:

  1. impl Point: Add - no type args provided
  2. Trait has 1 type param with default Self
  3. Substitute SelfPoint
  4. Result: impl Point: Add<Point>

Alternatives Considered

No Defaults

Require all type parameters to be specified.

Rejected: Too verbose for common cases like impl MyType: Add.

Inference Instead of Defaults

Infer missing type parameters from usage.

Rejected: Less predictable, harder to understand, may conflict with other inference.

References

  • Rust: trait Add<Rhs = Self> in std::ops
  • Scala: Default type parameters
  • Grammar: See grammar.ebnf § Generics