Object safety
A member that mentions Self in a parameter or its result, or that has no self, cannot be called on a trait-typed value, even though the trait stays a legal type.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
Object safety is checked at the call, not at the trait's declaration. A member that needs a second value of exactly the same concrete type cannot be called once that type has been erased to a trait, but the trait itself stays a perfectly legal type to hold, pass and store.
Example#
type Money with Show, Hash {
cents: Int
fn hash(): Int {
cents
}
}
const items: List<Show & Hash> = [Money(1), Money(2)]
print items.length()
fn describe(item: Show & Hash): String {
"{item} #{item.hash()}"
}
print describe(Money(3))Syntax#
fn <name>(other: Self): <Type> unsafe: needs a second value of the caller's own concrete type
fn <name>(): Self unsafe: promises to return the caller's own concrete type
static fn <name>(<parameters>): <Type> unsafe: `static`, so there is no value to dispatch on
fn <name>(...): <Type> safe: `Self` appears nowhere but in the receiver's own positionRules#
A member that mentions
Selfin a parameter or in its result cannot be called on a trait-typed value.Compare.min(other: Self): Selftakes and returns a second value of the caller's exact type, which a trait-typed value does not carry.Kompiliert nichtfn smaller(a: Compare, b: Compare): Compare { a.min(b) } // error: `min` cannot be called on a `Compare` valueThis rule applies to a default member exactly as it applies to a requirement.
minhas a body inCompare(if lessThan(other) { self } else { other }), and it is still rejected on aCompare-typed value, because the body is not what is checked - the signature is.A
staticmember has nothing to dispatch on, and is never called through a value.TryFrom.tryFromandFrom.fromarestaticrequirements; they are reached through the type's own name (Money.tryFrom(text)), never through a binding whose type happens to be the trait.The trait stays a legal type regardless.
List<Show & Hash>andfn describe(item: Show & Hash)compile and run:Show.showandHash.hashboth take onlyself, so both are safe, and nothing aboutCompare's unsafe members stopsShow & Hashfrom being used.Rejection happens at the call, so adding an unsafe member to a trait never breaks an existing trait-typed value.
List<Compare>is legal today even though every one ofCompare's members is unsafe; it has no callable member yet.
What this is not#
Object safety is not a property of the trait, checked once at its declaration. It is checked per call, against the type the value is known as at that point - so the same member is fine on a concrete type and rejected on the trait it was coerced to.
type Money with Compare, Equals {
cents: Int
fn compare(other: Money): Ordering {
if cents < other.cents { .Less } else if cents > other.cents { .Greater } else { .Equal }
}
fn equals(other: Money): Bool {
cents == other.cents
}
}
print Money(3).min(Money(5))type Money with Compare, Equals {
cents: Int
fn compare(other: Money): Ordering {
if cents < other.cents { .Less } else if cents > other.cents { .Greater } else { .Equal }
}
fn equals(other: Money): Bool {
cents == other.cents
}
}
fn smaller(a: Compare, b: Compare): Compare {
a.min(b)
}
// error: `min` cannot be called on a `Compare` valueMoney(3).min(Money(5)) type checks because both sides are known to be Money; the moment either one is only known
as Compare, min has nothing to promise its result is.
Related#
- Trait intersections - combining traits without changing which members are safe.
- Traits as types - what coercing to a trait type does and does not change.
- Witness tables - how a generic member, as opposed to a trait-typed value, still reaches every bound.