10. Traits and impls
Traits and impls use angle brackets for type application. impl Display<Point> reads "Display applied to Point", where Point is the implementing type.
struct Point
x: i32
y: i32
end
trait Display
def to_string(self) -> string
end
impl Display<Point>
def to_string(self) -> string
"(#{self.x}, #{self.y})"
end
end
An impl's method signatures must conform to the trait's. Each method's parameter and return types must equal the trait's declaration, with Self read as the implementing type, and an action's effect row must be a subset of the trait method's declared row: an impl may perform fewer effects than the trait promises, and never more. Bounded and static dispatch, below, type a caller against the trait's signature and charge the trait's effect row, and a divergent type or a wider effect row would return a type the caller never expects or perform effects it was never charged for. Both are rejected (H0617). A call on a concrete receiver resolves to that impl and is charged the impl's row: the trait's row is the ceiling a bound is typed against, and no floor every implementor pays. Narrowing an impl's row is therefore worth doing, at no cost to a caller that knows the receiver's type, and only the two bound-mediated forms sit at the ceiling.
A variable row removes that ceiling for the bound-mediated case. A trait method may declare its row as a single effect variable, def read!(self) -> bytes [e], in place of a concrete set. No ceiling applies, each impl declares whatever it performs, and a bounded generic names that row through its bound (<S: Stream[e]>, §6) and is charged the impl's own row at each instantiation. The impl's row must itself be concrete, the impl being the instance and a variable there having nothing to solve it (H0648), and a trait method's row is one or the other, never a variable among concrete effects (H0647). A supertrait's variable-row methods come through the bound with the rest of its surface, and the bound's variable covers them too. Under a concrete trait row every sentence above is unchanged.
A trait method signature may carry a default body. A type that impls the trait without overriding that method uses the default, and an inherent or impl override outranks it, per the inherent-wins coherence below. A default body may use the trait's other members on self, which dispatch to the implementing type:
struct Dog
age: i32
end
trait Greet
prop name(self) -> string # required - no default
prop greeting(self) -> string # carries a default body
"hi, #{self.name}"
end
end
impl Greet<Dog>
prop name(self) -> string
"rex"
end
# `greeting` is not overridden, so `Dog.greeting` uses the default: "hi, rex"
end
Properties (prop)
A property is a pure, self-only value read exposed with bare-dot syntax and no parentheses: 2.days, point.magnitude. It is declared with the contextual keyword prop inside a trait or impl body, shaped like a def and read like a field:
trait DurationUnits
prop days(self) -> Duration # signature; a trait prop may carry a default body
end
impl DurationUnits<int>
prop days(self) -> Duration
Duration.of_days(self)
end
end
x = 2.days # reads the prop — lowers to an ordinary call
y = 2.days() # error (H0567): `days` is a property — write `2.days`
Reading one with parentheses is H0567, here against the real datetime fluent units, which have the form sketched above:
use datetime
open datetime
def bad() -> datetime.Duration
2.days() # `days` is a property — write `2.days`
end
The call syntax states the semantics, one spelling per member:
| Spelling | Member kind | Meaning |
|---|---|---|
bare dot x.name | struct field, or prop | a pure value read, and nothing happens |
parens x.name() | def method | an invocation, computation asked for |
bang x.name!(…) | action def name! | an invocation with world effects |
Exclusivity is total: for any member one spelling is grammatical. A prop read with parentheses (2.days()) is an error (H0567), and a def method read without them is a no-field error with a "did you mean .name()?" hint.
The stdlib follows the same rule, and its attribute reads are properties. length and empty? on List, Map, Set, Range, string and bytes are read bare, xs.length and s.empty?, and never called. The rest of the collection surface asks for computation (xs.sort, s.trim, m.keys) and is def.
Constraints, enforced at registration:
- Self-only. A
proptakes one parameter,self, and no others (H0566). A member that needs arguments is adef. - Pure. A
propis a pure value read. Its name may not end with!and it has no effect row. Effects belong to actions. - No field collision. A
propmay not share a name with a field of the same type,x.namebeing ambiguous (H0566). This applies onopaquetypes too: apropshadowing a hidden field would makex.valuemean different things inside and outside the defining file. To re-expose a hidden field under a public name, rename the backing field and add apropthat returns it. - Kind conformance. An impl must declare a member with the same kind,
propordef, as its trait. Adefimplementing aprop, or the reverse, isH0568.
Props are declarable in traits, trait impls and inherent impls, and never on actors or at the top level. They may not declare method-own generics: a bare-dot read has no syntax to supply type arguments, and such a prop could never be instantiated. Bare-dot access works on a bounded generic receiver too (<T: DurationUnits> x.days, <T: Hash> x.hash), where the read dispatches through the bound's dictionary like a bounded method call. A prop read crosses the opacity boundary like any method: a prop on an opaque type is readable from any file, opacity gating construction and field access and no member read (§9).
A def with a property's form is an error (H0574). A pure, self-only, zero-argument def in an inherent impl or a trait declaration has the form of a property, and one spelling per member covers the def-and-prop split. It fails the check, with a machine-applicable def-to-prop fix that hanki check --fix applies. In a trait declaration the decision cascades, every impl inheriting the declared kind (H0568), and the rule and its diagnostic sit at the declaration and never at the impls; Hash.hash is a prop for that reason. The escape is semantic. A member with that form whose meaning is a transformation of the value, in place of an attribute of it, remains a def: the conversion prefixes to_, into_ and from_ are exempt by naming convention (to_iso(), to_seconds(), Display.to_string), and any other transformation is marked @transform def (sort, trim, hash_u64), in trait declarations as in inherent impls. A bare-dot read must never hide real work behind what looks like a field access. A misplaced @transform, anywhere the def-and-prop question cannot arise, on a top-level def, a member with arguments or generics, or a trait impl, is its own error (H0620), and stale markers cannot accumulate. Out of scope for H0574: trait impl members, whose kind the trait declaration mandates; meta and @encapsulated defs; members with method-own generics, a bare-dot read having no syntax for type arguments; and @intrinsic defs, that surface being the primitive conversion and math set, transformations by name. @intrinsic prop is itself a legal form, and str.length and List.length are declared that way. Where the member name shadows a field the fix is downgraded to a suggestion, to rename the backing field on an opaque type or drop the accessor on a transparent struct, the flip otherwise tripping H0566.
Generic implementations introduce type parameters implicitly:
struct Container<T>
item: T
end
impl Display<Container<T>> # T introduced from the parameter position
def to_string(self) -> string
"a container"
end
end
Constrained generics, where the bound makes T's own Show dictionary available inside the body:
trait Show
prop show(self) -> string
end
struct Crate<T>
item: T
end
impl<T: Show> Display<Crate<T>>
def to_string(self) -> string
self.item.show
end
end
Trait type parameters and associated types
A trait may declare type parameters and associated types, the machinery operator overloading builds on. The design record is docs/design/associated-types.md.
struct Thing
n: i32
end
trait Combine<B> # a type parameter, bound per-impl
type Out # an associated type, bound per-impl
def mk(self, b: B) -> Out
end
impl Combine<Thing, string> # positional: Self = Thing, B = string
type Out = bool # every declared associated type must be bound
def mk(self, b: B) -> Out # sigs may spell the names or the concrete types
self.n > 0i32
end
end
- The impl head is positional,
Selffirst. A trailing default,trait Add<Rhs = Self>, lets an impl omit the argument:impl Add<Period>meansAdd<Period, Period>, the same impl everywhere, for coherence, dispatch and api-diff, and no different one. A default is a bare type name:Self, an earlier parameter, or a concrete type. A composite default (C = List<Self>) is not yet supported (H0569). Arity outside the declared range, an unbound associated type, a binding the trait does not declare, and a binding in an inherent impl are allH0570. - Inside the trait and its impls the trait's names (
B,Out) are in scope and resolve to that impl's bindings. A binding may referenceSelf(type Out = Self), and a generic impl's binding projects through the instantiation:impl<T> Un<Box<T>>withtype Out = Tmakesun()on aBox<i32>ani32. An ordinary caller never names an associated type, and observes one as the checked result type of a call. The one other place it can be named is a bounded generic, through the projection below. Methods andtypebindings share the impl's member namespace, and a duplicate member is an error. - Coherence is per slot. Two impls of one trait conflict when every type-argument slot overlaps. Nominal slots overlap on equal base names; a bare generic parameter overlaps every type. Function and Future slots overlap only when their complete structural patterns have a common instantiation, including nested type arguments, repeated parameters and effect rows. Function parameter and result types are invariant for this check;
Neveris distinct from every inhabited type. Row parameters denote the complete matched row, with repeated occurrences required to agree. For example,(T) -> Toverlaps(i32) -> i32and is disjoint from(i32) -> string. Nominal slots retain the conservative head rule:Box<i32>andBox<string>overlap as standalone slots.Add<Instant, Duration>andAdd<Instant, Instant>coexist; a duplicate or overlapping tuple isH0516. - Argument-directed selection. Where several impls of one parameterized trait share the receiver's type, the argument types select the impl.
x.mk(y)trial-matches each impl's parameter types againsty's type, and coherence guarantees at most one fits. Selection requires the argument types to be concrete: an unsolved argument, an unsuffixed literal or an unannotated hole, and arguments no impl matches, areH0571, never an arbitrary pick. The selected impl's associated-type bindings give the call its result type, andAdd<Instant, Duration>'stype Output = Instantmakesinstant.add(duration)anInstant. A same-named method from a different trait is the ordinary ambiguity error. A property read has no argument to select by, and a bare-dotpropthat resolves through more than one impl, same trait or not, isH0619, which names the competing impls; rename your ownprop. One of them may come from a stdlib module the file never imported. The merged program includes every stdlib module any file in it imports, plus what those import, and once one file saysuse datetime, a fluent-units namedatetimedefines onint(days,hours,minutes,seconds,millis,micros,nanos,weeks,months,years) collides in every file of the program, in scope there or not. An extra module no file imports is not merged at all and cannot collide. A REPL session scopes the same way, over the modules it has imported so far (§22.6). - Operators.
a + banda - bon non-numeric operands are the flagship consumers. They select among the coreAddandSubimpls (trait Add<Rhs = Self> { type Output; def add(self, rhs: Rhs) -> Output }, moduleops) by the operand pair, each side defaulting on its own and never unified across, and the expression's type is the winner'sOutput. No matching impl isH0572, naming both operand types, and an impl with a builtin-primitiveSelfisH0573. The methods are puredefs, an impl being unable to widen a trait row, and an operator can never perform effects. Compound assignment inherits the dispatch,t += dbeingt = t.add(d), on a field target as much as a binding and at any nesting depth, and the §12 rebind rule pins theOutputto the target's type:t -= otherwhereSub'sOutputdiffers fromSelfis a type mismatch and no silent rebind. A bounded generic left operand dispatches through its bound, the route the explicita.add(b)takes. - Bounding a trait that declares associated types, and the
T.Assocprojection. A trait declaring associated types can be bounded like any other,<S: Stream>, and a signature in that body reaches the bound's associated types by projecting them off the parameter:def frame!<S: Stream>(s: S) -> Result<Frame, S.Error>. The projection is abstract inside the body and becomes the chosen impl's binding wherever the generic is instantiated, and one def serves transports whose error types differ. It names the parameter and never the trait, andS.Erroron a parameter carrying two bounds that both declareErroris refused as ambiguous in place of resolved by a pick (H0302, which also covers a name no bound declares and a head that is not a generic parameter). A bound the call cannot pin isH0555at the call site: an associated type in the result does not pin it, several impls being able to bind the same one. Pass a value of the type, or annotate one. - Associated-type bounds. A declaration may constrain every binding,
type Error: Display + Encode. Every impl'stype Error = Tmust satisfy each bound; a generic binding must carry and forward it, as inimpl<T: Display> Stream<Box<T>>. OmittingDisplayfromimpl<T>is invalid. The guarantee enters a bounded body automatically:<S: Stream>makesS.Errorusable throughDisplaywithout repeating the bound. A caller may add a narrower requirement in the declaration's existing generic list,<S: Stream, S.Error: Into<HttpError>>.S.Errorthere is a constraint subject. It adds no type parameter: generic arity and inference remain unchanged, and its dictionary follows the projection to the chosen impl binding. It must have a:bound, cannot have a default, and answers the same unknown/ambiguous-projection rules as any otherS.Error. An unsatisfied declaration binding or concrete instantiation isH0555at the binding or call. module.load!takes a plain trait, and that is a rule and no staging. A handle's method result must be a type the host knows, and a parameterized trait's is not:Module<Stream>would typem.read!()as the loaded module's ownErrorbinding, which the host cannot name and no compile-time check can see. The.hanki_manifestwire encodes neither type parameters nor associated types either, and the load-time compat check could not tell two parameterizations apart even were the type question answered. A host trait formodule.load!therefore declares neither.- A bound may name a trait's type parameters, and supplies them the way an impl head does.
Selfis the bounded parameter and is never written:<T: Add>ontrait Add<Rhs = Self>isAdd<T, T>, and<T: Add<Duration>>isAdd<T, Duration>; trailing defaults expand at the instantiation, whereSelfis the type the parameter took. The whole tuple selects the impl. Nested type arguments, function signatures and Future value and effect rows match structurally, and every occurrence of an implementation parameter must agree. A mismatch isH0555at the use, including for an implementation with no trait bounds of its own. For example,Add<Instant, Duration>andAdd<Instant, Instant>share a head,<T: Add<Duration>>atInstantresolves the first, and<T: Add>atInstantreports thatInstantimplements noAdd<Instant>in place of claiming it implements noAdd. Inside the body the trait's parameters are types (RhsisT), and its associated types are reached by theT.Assocprojection above. An arity outside the trait's declared range isH0570. Two bounds on one parameter naming the same trait areH0569: a parameter takes one dictionary per trait, and the second has nowhere to live. - Still staged (
H0569): a generic parameter as the right operand of+or-while the left is concrete, where the impl is selected by the pair and nothing there fixes what the parameter will be, and the method goes on the parameter instead; an operator on a parameter carrying no operator bound; static dispatch on a concrete type through a trait declaring type parameters,Thing.fresh(1)where the impl isMk<Thing, int>and nothing supplies the trait's arguments, though the same call through a bound,T.fresh(1)with<T: Mk<int>>, resolves because the bound supplies them; and default method bodies on a trait that declares type parameters or associated types.
Core into applies that machinery to explicit conversions. trait Into<To> declares @transform def into(self) -> To: Self names the source and the trait parameter names the target. A bound reads S: Into<HttpError> and fixes the result before the body calls s.into(). Coherence applies per slot, permitting one source to implement several targets. A concrete .into() has no value argument to select among several such impls and is ambiguous when the program contains more than one. A bounded call gets the target from its dictionary. There is no reverse From twin or blanket impl, and conversions are written by hand. The trait and bounded dispatch are ordinary front-end machinery shared by the bytecode and AOT tiers.
Two traits giving one type the same prop name is the ambiguity above, with nothing to select by:
trait First
prop tag(self) -> string
end
trait Second
prop tag(self) -> string
end
impl First<int>
prop tag(self) -> string
"first"
end
end
impl Second<int>
prop tag(self) -> string
"second"
end
end
def read(n: int) -> string
n.tag # H0619: resolves through both impls
end
Trait bounds and bounded dispatch
A trait bound on a generic parameter lets the body call that trait's methods on a value of the bounded type. <T: Eq> on a def head, or on an impl head, brings Eq's methods into scope for any T-typed value:
def same?<T: Eq>(a: T, b: T) -> bool
a.eq?(b) # resolves through the `Eq` bound on `T`
end
Without the bound, a.eq?(b) is a compile error: a bare T has no methods of its own. The call dispatches to the concrete type's impl of the bound trait. same?(3i32, 3i32) runs Eq<i32>'s eq?, and same?("a", "a") runs Eq<string>'s. Inference fixes the concrete type at each call site (§11), nothing is named in source, and there is no witness or dictionary syntax.
A parameter may carry several bounds, joined by + (<T: Hash + Decode>). Each brings its trait's methods into scope on a T-typed value, and each supplies its own dictionary at the call site, in the order written. Use a second bound for an unrelated trait; a supertrait comes with the first, and T: Hash brings Eq's methods too, Hash having Eq as its supertrait.
def labelled<T: Display + Eq>(a: T, b: T) -> string
if a.eq?(b) # method from the `Eq` bound
a.to_string() # method from the `Display` bound
else
"differ"
end
end
Interpolation goes through the same bound. A bare #{a} hole on a T-typed value with T: Display is a.to_string(), dispatching through the parameter's Display dictionary the same way on both tiers.
An impl-head bound (impl<K: Hash, V> Map<K, V>) is in scope for every method body in the block, and an inherent-impl method's own bound is in scope for that method's body:
struct Holder
tag: i32
end
impl Holder
def both_eq?<U: Eq>(self, x: U, y: U) -> bool
x.eq?(y) # resolves through the method-own `U: Eq` bound
end
end
All five forms dispatch end to end on both tiers: free def heads, impl-head bounds, generic trait-impl heads (impl<T: Display> Display<Wrap<T>>), an inherent method's own bound above, and a trait method's own bound below. A method may carry both an impl-head bound and its own. Two restrictions apply. A method's own generic must not reuse an enclosing impl-head generic's name: impl<K, V> containing def shadow<K>(…) is a compile error, and the method's parameter needs a different name. And trait-impl coherence is one impl per (trait, head type name): a concrete and a generic impl sharing a head, impl Encode<Box<i32>> alongside impl<T: Encode> Encode<Box<T>>, are rejected as overlapping. Trait dispatch resolves impls by the head's type name, specialization is unsupported, and the concrete case folds into the generic impl, matching the inherent-impl rule in this section's coherence notes. A head that names no type is refused (H0570): impl Wrap<T> and impl<T> Wrap<T> are blanket impls, which Hanki does not have, and a bare parameter leaves dispatch nothing to resolve by. A head that names a type may still be generic, and impl<T> Base<Wrap<T>> resolves by Wrap. That name is the module-qualified one, and a stdlib impl never reserves its head's bare name: your own Duration or Pair is a different type from datetime.Duration or pair.Pair, has its own impls, and leaves theirs intact.
A bounded free function or inherent associated function also retains its bounds when used as a value. For example, f: (i32) -> string = render instantiates render<T: Display> at i32 and captures that implementation's dictionary; a generic caller forwards its own dictionary. The same applies to qualified references and higher-order arguments, on bytecode, AOT and during constant evaluation. A type without the required implementation is rejected at the reference, even if the value is never called. An inherent associated function captures both its impl-head and method-own dictionaries, in declaration order. Its associated result type becomes available as soon as the arguments or surrounding function type select the implementation. A generic caller must carry the required trait with the same default-expanded type arguments; a missing or incompatible bound is H0555 at the call or function reference, including bounds forwarded through a generic implementation. A stored function's bound effect row follows the type that selects the implementation; a caller may forward it under its own row name and a concrete function annotation may select a concrete row.
A trait method may declare its own bounded generics: trait Encode { def encode!<S: Serializer>(self, s: S) }, including a static one like trait Decode { def decode<D: Deserializer>(d: D) -> Self }. An impl of that trait must declare the same bounds for that method. Parameter names are local, and the bounds must match in order, or it is a compile error, the call site supplying those dictionaries positionally. The method-own dictionaries are chosen at the call site and passed after the value arguments, and they are no part of the trait's fixed-arity dictionary: on a dynamic dispatch a generic impl's impl-head dictionaries arrive through the dictionary closure, while the method-own ones arrive from the caller.
# main.hk
open io
trait Sink
def put!(self, x: i32) -> () [io]
end
impl Sink<i32>
def put!(self, x: i32) -> () [io]
print!("#{x}")
end
end
trait Encoder
def encode_to!<S: Sink>(self, s: S) -> () [io] # method-own generic `S`
end
impl Encoder<i32>
def encode_to!<S: Sink>(self, s: S) -> () [io]
s.put!(self) # `S`'s dictionary is supplied by the caller
end
end
def main!(args: List<string>) -> () [io]
42i32.encode_to!(0i32) # prints 42
end
The == and != operators on a generic-T operand follow the same rule. a == b where a: T dispatches through T's Eq bound as a.eq?(b) does, and != negates it. They therefore require T: Eq, and == or != on an unbounded generic parameter is a compile error, there being no Eq impl to dispatch through. A concrete == works the same way: it dispatches through the type's Eq, synthesised on demand or hand-written, with no structural fallback, and it too is a compile error where the type cannot have Eq (see §20's eq? row).
The structural comparison underlying == on an aggregate is depth-bounded. A comparison that recurses past a fixed cap traps as a catchable runtime error, an actor death routed to the supervisor and no silent whole-process abort. A deeply-nested value would otherwise overflow the actor thread's stack. A value is always finite: cyclic values cannot be constructed, and values are acyclic (§12). The cap is a stack-overflow guard set far deeper than any legitimately-nested value, and it fires identically on the bytecode, AOT and comptime tiers. The same bound governs the other structural trait recursions, Ord comparison (<, cmp), Hash, and Display rendering, which walk a value's nesting the way == does and trap at the same depth in place of exhausting the stack.
Supertraits
A trait may declare a supertrait with trait Sub: Super. The compiler enforces two things:
- Impl requirement.
impl Sub<T>is valid only whereimpl Super<T>also exists. You cannot implement the subtrait for a type without implementing its supertrait for that type. - Transitive bound. A
<T: Sub>bound brings the supertrait's methods into scope on aT-typed value and requiresT: Superat every instantiation. A body bounded byT: Submay therefore call both the subtrait's and the supertrait's methods, each dispatched through the concrete type's impl. Where the chain reachesEq,==and!=resolve on aT: Suboperand too.
trait Hash: Eq # every Hash type is an Eq type
prop hash(self) -> u64
end
def bucket?<T: Hash>(x: T, y: T) -> bool
x.eq?(y) # `Eq`'s method, in scope via the `Hash: Eq` bound
end
One supertrait per trait. The relationship is transitive: a chain A: B, B: C makes C's methods available to a T: A bound and requires impl C<T> wherever impl A<T> appears, each link's impl requirement chaining to the next. The supertrait position accepts a module-qualified name like every other cross-module item reference (§14), and trait Comparable: eq.Eq works without an open eq.
Inherent impls
impl <Type> ... end, with no trait and no <args>, is an inherent impl: methods and associated functions attached to a type without a trait. The implementing type can be a built-in (impl i32), a user struct or sum (impl Email), or a module-qualified name (impl foo.Bar). Declaration order is free, as it is for actors (§15): an impl may be written above the struct or type it is for, and a struct field or a variant payload may name a type declared later in the file.
struct Email
text: string
end
impl i32
@transform def doubled(self) -> i32
self + self
end
end
impl Email
prop empty?(self) -> bool
self.text.length == 0
end
end
A method, whose first parameter is self, dispatches through value.method(). The two members above follow the one-spelling rule of this section: the attribute read is a prop, and the prop-shaped transformation is marked @transform (H0574). An associated function, with no self parameter, dispatches through <Type>.fn(args): the receiver position names the type, and the call resolves to the inherent associated function of that name. Qualified types work in both calls and inherent function values: model.Colour.tag(x), xs.map(model.Colour.tag) and bytes.BytesBuilder.new!.
impl FromString<int>
def parse(s: string) -> Result<int, ParseError>
...
end
end
x = int.parse("42") # Ok(42) — a trait static, reached by type name
e = Email.parse("a@b") # Email("a@b") — same syntax on user types
Cross-tier conversion methods (i32.to_f64, int.to_i64, and the rest) are inherent methods on the source type, following the same convention.
A trait may also declare an associated function, a method whose first parameter is not self, and <Type>.fn(args) resolves it through the type's impl of that trait, as it does for an inherent one. An inherent associated function of the same name takes precedence: it is chosen, and no ambiguity is reported. The type is named in the receiver position, a Self in the function's signature is pinned to it, and no return-type inference is needed. The same call works through a bounded type parameter. Inside a <T: Trait> body, T.fn(args) dispatches to the concrete type's impl through the trait dictionary, the no-self counterpart of the value.method() dispatch above, and what lets a generic codec construct a value of its own type parameter with T.decode(bytes, pos). There is no inline-type-argument form (§11), the type is always named, and a generic instantiation (List<i32>) is reached through a type parameter and never spelled in receiver position.
# main.hk
open io
trait FromByte
def from_byte(b: u8) -> Self
end
impl FromByte<u32>
def from_byte(b: u8) -> Self
b.to_u32()
end
end
def widen<T: FromByte>(b: u8) -> T
T.from_byte(b)
end
def main!(args: List<string>) -> () [io]
direct = u32.from_byte(5u8) # concrete: resolves FromByte<u32>
generic: u32 = widen(7u8) # T pinned to u32 by the binding; T.from_byte dispatches via the bound
print!("#{direct == 5u32 and generic == 7u32}") # true
end
Coherence: at most one inherent item per (target type, name) pair. Methods and associated functions share one namespace per target type. An inherent method named the same as a trait method that also matches the receiver's type takes precedence, being chosen with no ambiguity reported, under the same inherent-wins rule the associated-function dispatch above follows. Only a clash between two trait methods, two traits defining the same method for one type, remains ambiguous; rename one or qualify explicitly.
Generic inherent impls
impl<T> Container<T> ... end attaches inherent methods to a generic container. The impl's type parameters are written in angle brackets on the impl head and referenced in the head's instantiation, and methods inside use them in their signatures the way a generic def<T> does. At each method call site the impl's parameters are unified with the receiver's concrete arguments, and the method's signature is substituted accordingly.
impl<T> Option<T>
def unwrap_or(self, default: T) -> T
match self
Some(v) -> v
None -> default
end
end
end
Some(7i32).unwrap_or(0i32) # T pinned to i32 from the receiver
One impl per base target type (Option, List, and the rest). A generic impl and a concrete impl Option<i32> on the same target are rejected as mixing generic forms; pick one. Bounded generic inherent impls (impl<T: Show> Container<T>) carry the same bounds and dictionaries described above.