Traits
A trait is a capability a type comes with. A single-method trait is named after its method, there is no inheritance, and operators are traits.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
A trait is a list of members a type provides. A type comes with a trait at its declaration, or gains it afterwards
through extend. There is no inheritance, no base class and no implements keyword: with is the only word for it.
Example#
trait Area {
fn area(): Float
fn describe(): String {
"a shape with area {area()}"
}
}
type Square with Area {
side: Float
fn area(): Float {
side * side
}
}
extend Square with Show {
fn show(): String {
"Square({side})"
}
}
const square = Square 2.0
print "{square} {square.describe()}"Syntax#
[public] trait <Name>[<parameters>] [with <supertraits>] {
fn <name>(...): <Type> a requirement
fn <name>(...): <Type> { ... } a default member
var fn <name>(...): <Type> a requirement that changes the receiver
static <name>: <Type> a constant every implementation provides
static <name>: <Type> = <value> a constant with a default value
}
type <Name> with <Trait>, (<Trait>, <Trait>) by <field> { ... }
extend <Name> with <Trait> { ... }
extend <Name> { ... }
extend<Item> List<Item> with <Trait> where Item: <Bound> { ... }
fn draw(shape: Area) a trait as a type
fn audit(entry: Show & Encode) an intersection of traits
fn sum<Item: Add>(values: List<Item>) a boundRules#
A trait with one required method is named after that method.
Hashhashash,Equalshasequals,Comparehascompare,Showhasshow,Addhasadd,Closehasclose,Iteratehasiterate. There are no-ableadjectives:type Money with Equals, Hash, Comparereads as what it is. A trait that is mainly used as a type is a noun instead:Iterator,Accumulator,Source,Sink.Iteratehas one required method,iterate, and is named after it.withis the only keyword for "implements" and for supertraits.trait Compare with Equalssays that every type withComparealso hasEquals.A member without a body is a requirement; a member with a body is a default. A default written in terms of the required members stays correct for every implementor.
That holds for a constant as well.
static zero: Selfwithout a value is a constant every implementation provides as astaticconstant of its own - in the type, in theextendthat implements the trait, or in any otherextendof the type - and one with a value is a default an implementation may replace. A body that is generic over the trait reads it through the type parameter, and which constant that is follows from the type the parameter stands for:Numericrequireszeroandone, soScalar.zeroisInt.zeroin one instance andFloat.zeroin the next.trait Empty { static empty: Self } type Label with Empty { text: String static empty = Label "" } fn emptyOf<Value: Empty>(): Value { Value.empty } const blank: Label = emptyOf() print blankA
static fndoes not stand in for a required constant, and a constant without a value anywhere but in a trait is an error.Kompiliert nichttype Label { text: String static empty: Label } // error: A constant of the type needs a valueA bound is written
where Item: Hash & Equalsor inline as<Item: Hash>. A member may carry awhereclause of its own, and then the member exists only where the clause holds - it is not a requirement on implementors.fn toSet(): Set<Item> where Item: Hashextendadds constants and functions, and nothing else. A field or acasein anextendis an error, because exhaustiveness and the generated constructor have to be decidable from the declaration alone.extend String { fn shout(): String { "{toUpperCase()}!" } } print "hello".shout()Coherence: you may write
extend X with Traitonly if your package ownsXor ownsTrait. Two implementations of one trait may never overlap.extendwithout a trait is part of the type when the type belongs to your package - or to this one - in whatever file it is written. For a type of another package the file that uses the member names it,use String.shout from "acme/text", the same form a case import takes; and what a trait puts on a type it does not own is visible where the trait itself is a name of the file. See extend for both halves.A trait can be used as a type. Whether the call is dispatched statically or dynamically is the implementation's business and is not observable.
&intersects traits in a type position, and only traits can be combined -&is to traits what|is to literal types.Coercion to a trait type is the only subtyping in the language, and there is no variance. A
List<Square>is not aList<Shape>; the list is built as one:trait Area { fn area(): Float } type Square with Area { side: Float fn area(): Float { side * side } } const shapes: List<Area> = [Square(2.0), Square(3.0)] print shapes.length()Object safety is checked per call, not per type. A member that mentions
Selfin a parameter or in its result, or that isstatic, cannot be called on a trait-typed value - but the type stays usable as a type, soList<Show & Hash>is legal and only the calls that have no meaning are rejected.Operators are traits.
+isAdd.add,-isSubtract.subtract,==isEquals.equals,<isCompare.compare,a[i]isIndex.at,a[i] = visMutableIndex.set,a[from..to]isSlice.slice, and string interpolation isShow.show.&&,||and!are the exception: they are built intoBool, they short-circuit, and they cannot be overloaded.A type parameter of a
typeor atraitmay have a default (trait Add<Other = Self, Output = Self>), which is whywith AddmeansAdd<Self, Self>and nobody writes it out. Afnhas no such defaults: its type arguments come from the call.One type may carry a trait with a parameter several times, and each of them is one
extend. That is the overload form the language has (Where are my overloads), and which one a call means is decided by everything the call says: every argument that is not a closure, and the type the call is expected to produce. Two bodies of two instantiations cannot stand in onetypebody, because a type has one namespace of members.trait Store<Component> { fn valueOf(): Component? fn attach(value: Component): Int } type Game { number: Int text: String } extend Game with Store<Int> { fn valueOf(): Int? { number } fn attach(value: Int): Int { value } } extend Game with Store<String> { fn valueOf(): String? { text } fn attach(value: String): Int { value.byteLength() } } const game = Game 1, "one" const named: String? = game.valueOf() print "{game.attach(7)} {named}"Where nothing in the call tells them apart, the checker names them instead of taking the first:
`valueOf` fits more than one implementation here.bydelegates a trait to the one field of a single-field type, and it binds to the element of thewithlist directly in front of it - a trait alone, or a parenthesised group of them - never to the whole list.type Seconds with Show, (Add, Subtract) by value, Compare by value { value: Int } const total = Seconds(5) + Seconds(2) print totalShowis derived here;Add,SubtractandCompareare forwarded tovalue. A trait that is neither derived, delegated nor written by hand is not available, which is the point:Seconds * Secondsdoes not compile, because that would be square seconds.byneeds a type with exactly one field, and the name afterbynames that field. A type with two fields cannot delegateAddto either one, and a type with cases has no field of its own to name.Trait.memberreads the trait's own members first, then the members of implementations whose target is the trait itself. That is what makesList.of(1, 2)andList.from(iterable)work. Two such implementations are an ambiguity error, and the fix is to name a type.A
shared typecan only implement ashared trait. So a value of a trait type is always a value: nobody changes it while you hold it, and it can be passed to another task. For the same reason a value whose type holds a shared object does not become a value of an ordinary trait (see Shared types, rule 7).
What this is not#
A trait is not an interface with inheritance. There is no base class, no super, no overriding of a concrete
implementation. A type either provides a member or takes the trait's default.
A trait is not Hashable. The name is the method, not an adjective:
type Money with Equals, Hash {
cents: Int
fn equals(other: Money): Bool {
cents == other.cents
}
fn hash(): Int {
cents
}
}
print Money(1).equals(Money(1))type Money with Hashable {
cents: Int
}
// error: Unknown type `Hashable`with does not mean "and also these type parameters". with introduces traits. Type parameters go in angle
brackets, and bounds go after where or inline.
An extend is not a monkey patch. For a type of another package the file that uses the member names it, and two
members of one name for one type make calling it an error until as renames one of them where it is imported.
A blanket implementation is not a fallback. extend<Source, Target> Source with Into<Target> where Target: From<Source> covers every type, is allowed only for a trait the package owns, and can never coexist with a second
blanket implementation of the same trait - overlap is decided conservatively, so two of them always collide.
Related#
- Declaring a type - the type a trait is given to.
- Cases and match - the other half of what a
typecan be. - std/core - where
Equals,Compare,Hash,Showand the operator traits live. - Coming from Rust -
impl Trait for Typeagainstwithandextend.