lang::opaque_nominal_alias¶
type Name = new Repr declares a distinct nominal type over an unchanged runtime representation, erased before lowering so no tier sees one. It inherits equality, ordering, hashing, and formatting - which describe the value both sides share - and nothing else: arithmetic needs the alias's own impl Add, and the representation's methods are not in scope. .into() converts to and from its own representation; any other pair needs impl From.
A transparent alias (type Id = i64) is another spelling of its target.
An opaque alias puts new before the target and declares a type of
its own:
UserId and i64 are now different types, and so are UserId and
Score. Nothing converts between them on its own:
let id: UserId = 41.into()
let n: i64 = id // error[GT0001]: type mismatch: expected `i64`, found `UserId`
let s: Score = id // error[GT0001]: type mismatch: expected `Score`, found `UserId`
fn charge(user: UserId, amount: Score) { }
charge(amount, user) // rejected: the arguments cannot be swapped by accident
The distinction is entirely in the checker. The runtime value is exactly the representation, so an opaque alias costs nothing, changes no layout, and behaves identically on the bytecode VM, the JIT, and a native build.
Converting¶
.into() crosses between an alias and its own representation, in both
directions. The two are one value, so the conversion carries no work:
Every other pair needs a conversion written for it. Reaching for
.into() without one is reported at check time rather than failing when
the program runs. So is a call no use site gives a target: the target of
.into() comes from the annotation, parameter, or return that receives
it, never from the receiver.
let s: Score = id.into()
// error[GT0066]: no conversion from `UserId` to `Score`
// = help: write `impl From<UserId> for Score`
impl From<UserId> for Score {
fn from(u: UserId) -> Score { (u.value() * 10).into() }
}
let s: Score = id.into() // runs the impl
What an opaque alias inherits¶
Nothing from its representation's behaviour. Hiding what the type is made of is the whole point, so the representation's API is not part of the alias:
type Name = new String
let n: Name = "ada".into()
n.len() // error[GT0002]: no method named `len` found for type `Name`
let a: UserId = 1.into()
let b: UserId = 2.into()
a + b // error[GT0003]: cannot apply `+` to `UserId`
Both are deliberate. Adding two user ids is rarely meaningful, and where it is, it is worth writing down. To reach the representation's surface, convert to it, or give the alias a method of its own.
Equality, ordering, hashing, and formatting are inherited. Those
describe the value, which the alias and its representation genuinely
share, and they are what let an alias be a Map or Set key, sort, and
print:
let mut seats: Map<UserId, String> = Map::new()
seats.insert(a, "front")
println!("{} {} {}", a < b, a == b, a) // true false 1
Its own impl¶
An opaque alias carries inherent and operator impl blocks like any
other type:
impl UserId {
fn value(&self) -> i64 { self.into() }
fn next(&self) -> UserId { (self.value() + 1).into() }
}
impl Add for UserId {
fn add(&self, other: UserId) -> UserId {
(self.value() + other.value()).into()
}
}
Choosing between the three¶
| Want | Use |
|---|---|
| A shorter spelling of an existing type | type Id = i64 |
| A distinct type over the same representation | type Id = new i64 |
| Named fields, or a layout of its own | struct Id { .. } |
Serialization¶
A struct field typed by an alias serializes as its representation, and decoding converts back, so an alias is usable in the types a program actually exchanges: