Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-621 · Case file with fixtures · Case 593 of 694 · Compiler evidence

Generic Parameter Defaults Cannot Be Defined in Terms of Self

A generic default is an argument chosen before the final Self type is formed. Use an independent concrete default, a named recursive type, or a constructor alias with explicit meaning.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Argument omission was defined in terms of the completed Self even though selecting that missing argument is necessary to form Self in the first place.
First discriminating check
Use an independent semantic default, move recursion into an explicit field, or name a preferred concrete composition with an alias.

Generic defaults make a type convenient to name when one argument has a common choice. They do not run after the complete type has somehow been assembled. The failing fixture writes T = Box<Self> while defining a struct. Rust rejects that circular default with E0735.

Default selection is part of forming the type

I read Container<T = Fallback> as a rule used when the caller omits T. Rust must choose that argument in order to know which Container<...> is meant. At this point, using Self asks for the finished type before its missing argument has been chosen.

The official E0735 page states that defaults on structs, enums, and unions cannot use Self. The Reference describes generic parameters and defaults, including where defaults are permitted and how parameter kinds are ordered.

This is different from using Self inside an implementation or trait method, where an implementing type is already known. The same spelling participates in different phases of type formation.

Choose a default with independent meaning

The repaired fixture uses () as a neutral, independently nameable default. That is only one possible design. A collection may default its allocator, a parser may default an error type, and a protocol wrapper may default a marker policy.

I choose a default because it represents the normal semantics, not because it is easiest to make compile. Callers who omit the argument will depend on it in public type annotations, documentation, and semver-sensitive APIs.

If recursion is actually wanted, I define it explicitly. A non-generic Node can contain Option<Box<Node>>. A generic node can contain Option<Box<Node<T>>> in a field while still taking T from the caller. Both forms make the recursive edge visible instead of hiding it in argument omission.

Aliases can express a preferred composition

Sometimes there is a useful general type but one preferred recursive configuration. A named type alias can spell that composition at the API boundary. It gives documentation a stable vocabulary and keeps the underlying generic definition free of a circular default.

Aliases do not create a distinct nominal type. If invariants or separate implementations matter, a newtype or dedicated struct is stronger. I decide this from the domain rather than treating aliases as a universal workaround.

Constructors need equal attention. Type inference may not know which default is intended when every field is None or otherwise unconstrained. The repaired fixture annotates the variable. Convenient constructor functions can return the intended specialization and avoid repetitive annotations.

Public defaults are compatibility decisions

Changing a default can alter which implementation, trait bounds, size, or behaviour callers receive when they omit an argument. Even if explicitly parameterised code remains valid, inferred or abbreviated uses may change meaning.

I therefore document the default in the type’s public explanation and test both the abbreviated and explicit spellings. API review asks whether users should rely on omission or whether an explicit alias communicates the supported configuration better.

Defaults can also increase diagnostic distance. A trait-bound error may mention a type the caller never wrote because it came from a default. Clear aliases and rustdoc examples make that expansion visible.

Macro-generated types need phase awareness

A derive or schema macro may generate a wrapper and attempt to base its default on the wrapper itself. The emitted tokens look plausible but carry the same circularity. I inspect expanded output, separate the base generic definition from chosen compositions, and keep recursive fields explicit.

Generated names should not rely on implicit defaults for wire compatibility. Schema versions, ownership modes, and error policies are too important to change silently when a generator evolves.

The Reference section on Self types is useful when deciding whether Self is available in a particular context. I verify the exact context rather than generalising from an impl example.

My E0735 checklist

  • Which type parameter default mentions Self?
  • Is the intended relationship actually recursive, or only a convenient specialization?
  • Can the default be an independent concrete type with clear semantics?
  • Should recursion appear in a field as Box<Node<T>> instead?
  • Would a public alias or constructor express the preferred composition better?
  • Can inference determine the omitted argument at construction sites?
  • Is the default already part of a public compatibility promise?
  • Did a macro generate the circular reference far from the source schema?

The core principle is that a default helps construct a concrete type; it cannot depend on that still-unfinished type. E0735 breaks the circle. I then model recursion explicitly and give common configurations names that reveal their actual meaning.