Generics, Interfaces, Embedding, and Methods
This file is part of the Nocter language specification. The specification entry point is README.md.
v0 Scope
Nocter v0 includes generic type parameters, associated functions, inherent impl blocks, receiver methods, contract-only interface declarations, explicit interface conformance declarations, and Self type syntax inside inherent member and interface method contexts.
Nocter v0 does not include traits. Embedding is an adopted future composition feature, but it is not part of the v0 implementation contract. Generalized ... spread, rest capture, variadic capture, and typed literal construction are specified as a separate future direction in Future Literal Definitions and Spread.
Not part of v0:
traitdeclarations- embedding declarations such as
...Typeandpub ...Type - typed literal construction such as
Vec [1, 2, 3] - generalized
...spread, rest capture, and variadic capture forms - generic bounds such as
T: Interface - interface-bound method lookup
- interface objects such as
dyn Printable - interface inheritance, associated types, default methods, blanket impls,
specialization, and
whereclauses - code reuse through interfaces
trait is not a reserved keyword in v0. It is lexed as an identifier. A source form that starts a top-level item with trait is diagnosed as removed syntax, but the spelling remains available as an ordinary identifier in positions such as a function name.
interface is a reserved keyword.
Impl Blocks
Adopted: impl associates receiver methods and destructor members with a nominal type. It is not a class declaration and does not introduce inheritance.
Associated functions are declared at top level with a qualified function name. They have no receiver and are called through the type.
pub func WordStats.empty(): WordStats {
return WordStats {
bytes: 0,
lines: 0,
words: 0,
}
}
let stats = WordStats.empty()
method inside an impl defines a receiver method. The receiver is explicit and appears before the method name.
impl WordStats {
pub method &+self.add_byte(byte: u8): void {
self.bytes += 1
}
pub method &+self.add_word(): void {
self.words += 1
}
}
Generic impl blocks declare their own type parameters before the target type. Those parameters are in scope for the impl target, method signatures, drop members, and member bodies.
impl<T> Box<T> {
pub method self.value(): T {
return self.value
}
}
stats.add_word()
Self is type-position syntax inside an inherent impl block and inside a qualified associated function declaration such as func WordStats.empty. It is not an ordinary identifier and is not resolved through normal name lookup. In impl WordStats or func WordStats.empty, Self means WordStats.
self is the fixed receiver name in method and drop member declarations. Self remains type-position syntax. The restrictions on Self are specified in Values and Types.
The target of an impl block must be a nominal type declaration, such as a struct or enum. An impl block cannot target a type alias because aliases do not create distinct types.
type Count = i32
impl Count {
...
}
// error: Count is a type alias, not a nominal type
impl Interface for Type declares explicit conformance to an interface. It is not an inherent impl block and cannot contain members.
impl Printable for User
impl Printable for User {}
Generic conformance declarations use the same impl generic parameter list:
impl<T> Source<T> for Box<T>
The implementing methods are ordinary public inherent methods on the target type.
impl User {
pub method &self.print(): i32 {
return 0
}
}
Initial receiver forms:
method &self.name(...): Return
method &+self.name(...): Return
method self.name(...): Return
Meaning:
&Selfis a readonly receiver.&+Selfis a readwrite receiver.Selfis a consuming receiver. It requires copy or explicit move according to the normal ownership rules.- Calling a
&Selfmethod borrows the receiver readonly. - Calling a
&+Selfmethod borrows the receiver readwrite and requires a writable receiver place. - A newly created owned temporary may be used as a
&+Selfreceiver for that single method call because it has no existing aliases. - Calling a
Selfmethod consumes or copies the receiver according to the receiver type. - Borrow-like values derived from a temporary receiver cannot escape the current statement.
Call rules:
Type.function(args)calls an associatedfunc.value.method(args)calls amethod.- Associated function and method arguments follow the positional argument rules in Control Flow.
Type.method(&value, args)andType.method(&+value, args)are invalid in v0.value.function(args)is invalid whenfunctionis only an associatedfunc.func Type.nameandmethodshare the same member namespace for a type. Defining both with the same member name for the same type is an error in v0.- Enum variants also occupy the type member namespace. An associated
funcormethodcannot reuse an enum variant member name in v0. - If method lookup finds multiple valid inherent candidates, the call is ambiguous and is a compile error.
- v0 has no qualified method-call escape hatch for ambiguity resolution.
file.write_text("hello")? // OK: method call
File.write_text(&+file, "hello")? // error: methods are not UFCS functions
Method Lookup
Adopted: method lookup is deliberately small and deterministic in v0.
For value.method(args), the compiler first determines the static type of value.
If the receiver has a concrete nominal type, the compiler looks only for inherent methods declared in impl Type blocks for that nominal type.
If the receiver is a generic type parameter, v0 has no interface-bound method lookup. A method call through an unconstrained generic receiver is invalid unless a future feature supplies a bound and lookup rule.
Lookup order:
- 1. inherent method on a concrete nominal receiver type
- no candidate, producing a compile error
The compiler does not search visible interface conformance declarations to resolve value.method(args) in v0. This avoids import-dependent method lookup and keeps calls readable from the receiver type.
Initial implementation order:
- 1.
impl Type { ... }receiver methods Selfinsideimplandfunc Type.name- associated function declarations such as
func Type.function(...) - associated function calls such as
Type.function(...) - method declarations
- method calls such as
value.method(...)
Interface Contracts
An interface is a contract-only nominal declaration. It may be private, pub, or pub(nocter) like other top-level definitions. Every member inside an interface must be explicitly marked pub.
pub interface Printable {
pub method &self.print(): i32
}
Rules:
- Interface members are method signatures only.
- Interface method signatures cannot have bodies.
- Interface members cannot be private or
pub(nocter). - Interfaces cannot declare fields, associated functions,
dropmembers, default methods, associated types, or reusable code. Selfinside an interface method signature means the eventual conformance target type for structural checking.- An interface conformance declaration is written
impl Interface for Type. - A conformance declaration may omit
{}or use an empty{}body. - A conformance declaration body cannot contain members.
- The conformance target must be a nominal
structorenum. - Typechecking verifies that the target has a public inherent method with the same name and signature for every interface method.
- Method parameter names do not participate in conformance. Receiver type, parameter types, and return type do.
- Conformance is explicit. A type with matching methods does not satisfy an
interface unless source contains
impl Interface for Type.
Example:
interface Reader {
pub method &+self.read_byte(): i32!
}
struct File {
fd: i32
}
impl File {
pub method &+self.read_byte(): i32! {
...
}
}
impl Reader for File
This model prevents accidental conformance while keeping the contract check structural. It also keeps code reuse out of v0: interface declarations describe requirements only.
Interface And Embedding Separation
Adopted: Nocter separates contract checking from implementation reuse.
An interface describes a public capability. It does not store data, provide method bodies, forward calls, inject members, or reuse code.
Embedding owns another value inside a struct and promotes only that value's public contract through the embedding owner. It is Nocter's planned composition-based reuse feature. It is not inheritance, not a trait, not a mixin, and not implicit interface conformance.
This separation is part of Nocter's core direction:
interfaceanswers "what public capability does this type promise?"embeddinganswers "what contained value does this type own and expose?"
The two features may work together, but neither feature includes the other. A type that embeds a value does not automatically conform to an interface, and an interface does not provide reusable implementation.
Generics
Adopted: generic type parameters use angle brackets.
struct Buffer<T> {
...
}
func first<T>(items: &[T]): T? {
...
}
Generic parameter grammar in v0:
GenericParameters = "<" GenericParameter ("," GenericParameter)* ">"
GenericParameter = Name
Generic bounds are deferred after v0. The parser must diagnose a generic parameter colon such as T: Printable as a deferred feature.
Generic implementation uses monomorphization. Each concrete instantiation is compiled as concrete code.
Buffer<i32>
Buffer<String>
This keeps generic dispatch static, avoids runtime type metadata for basic generics, and fits the no-runtime direction.
Initial generic scope:
- type parameters on structs
- type parameters on functions
- type parameters on
implblocks where needed - compile-time monomorphization
Deferred generic features:
- inline bounds such as
T: Printable - multiple bounds such as
T: A + B - full
whereclauses - higher-kinded types
- generic associated types
- const generics beyond the minimum needed for fixed-size arrays
Embedding
Adopted future design: embedding is Nocter's privacy-preserving composition feature.
Embedding declares that a struct stores an unnamed value of another struct type. The embedding owner may use only the embedded type's public surface. Private fields, private methods, private associated functions, and other implementation details of the embedded type are not promoted, even when the embedding owner is declared in the same module.
Initial syntax:
struct Profile {
...User
pub ...Article
visits: i32
}
In the embedding subset, leading ... is recognized only in struct bodies and struct literals. Broader future uses of ... are defined separately in Future Literal Definitions and Spread so embedding does not become a hidden import, macro, or general delegation mechanism.
Meaning:
...Userstores aUservalue and promotesUser's public instance members to theProfileimplementation scope only.pub ...Articlestores anArticlevalue and promotesArticle's public instance members as publicProfilemembers.- The embedded values do not receive source-level field names such as
user,article,User, orArticle. - The embedded types do not know that they are embedded.
Embedding is directional. If Profile embeds User, Profile owns a User value. User does not gain access to Profile, does not see Profile's private or public members, and does not dispatch to Profile overrides. self inside a promoted User method is still the embedded User, not the outer Profile.
Example:
struct User {
id: u64
name: String
pub age: i32
}
impl User {
pub method &self.print(): void {
print(self.name)
}
}
struct Article {
title: String
pub text: String
}
struct Profile {
...User
pub ...Article
visits: i32
}
impl Profile {
method &self.run(): void {
self.print() // OK: User.print is public, promoted inside Profile
self.id // error: User.id is not public
self.age // OK: User.age is public, promoted inside Profile
self.title // error: Article.title is not public
self.text // OK: Article.text is public and promoted
self.visits // OK: Profile's own private field
}
}
Outside Profile's implementation scope:
let profile = make_profile()
profile.print() // error: User was embedded privately
profile.id // error: User.id is private to User
profile.age // error: User was embedded privately
profile.title // error: Article.title is private to Article
profile.text // OK: Article was embedded publicly and text is public
profile.visits // error: visits is private to Profile
Embedded Initialization
Struct literals initialize embedded values with embedded initializers:
func Profile.new(
name: String,
age: i32,
title: String,
text: String,
): Profile {
return Profile{
...User.new(move name, age),
...Article.new(move title, move text),
visits: 0,
}
}
Rules:
- An embedded initializer has the form
...expr. exprmust have the embedded target type after generic substitution.- Every embedding declaration must be initialized exactly once.
- Unknown embedded initializers are compile errors.
- Duplicate embedded initializers are compile errors.
- Field initializers and embedded initializers are evaluated left to right in the order written in the struct literal.
- If initialization fails through postfix
?, already initialized ordinary fields and embedded values are dropped in reverse initialization order. - A struct may not embed the same concrete target type more than once in the initial design.
Promotion And Visibility
Embedding promotion is not ordinary import, not ordinary module-private access, and not a named field.
Only the embedded type's public instance members are promotable:
- public fields
- public
&selfreceiver methods - public
&+selfreceiver methods
Not promoted in the initial design:
- private fields
- private methods
- associated functions
dropmembers- enum variants
- type aliases
- nested declarations
- consuming
selfreceiver methods pub(nocter)members
Consuming receiver methods are not promoted initially because calling one would partially move the embedding owner. A future design may allow them only after partial-move and drop-state rules for embedded values are fully specified.
For ...T, promoted members are visible only inside the embedding owner's implementation scope:
- inherent methods in
impl Owner drop &+selfinimpl Owner- qualified associated functions declared as
func Owner.name
They are not visible to unrelated top-level functions in the same module. This is stricter than ordinary module-private visibility because embedding is a type-contract boundary.
For pub ...T, promoted members become public members of the embedding owner. They are available anywhere the owner type is visible and the promoted member's use satisfies normal borrow, assignment, and ownership rules. Reading or writing a promoted public field reads or writes the field inside the embedded value.
Name Collisions
Embedding does not provide renaming, aliasing, override order, or explicit disambiguation.
A promoted member name must not collide with:
- an ordinary field declared directly by the embedding owner
- an inherent method declared directly by the embedding owner
- a member promoted by another embedding
- another member promoted by the same embedding target
Collisions are compile errors regardless of whether the colliding members would be private or public after promotion.
Rationale: a collision means the composed API has stopped being self-evident. Nocter requires the author to choose clearer names at the source types instead of adding local rename syntax.
Interface Interaction
Embedding does not create implicit interface conformance.
pub interface Printable {
pub method &self.print(): i32
}
struct User {
id: u64
}
impl User {
pub method &self.print(): i32 {
return 0
}
}
struct Profile {
pub ...User
}
impl Printable for Profile // explicit conformance is still required
When checking an explicit impl Interface for Owner, public methods promoted by pub ...T may satisfy interface requirements as public Owner methods. Methods promoted by private ...T cannot satisfy a public interface because they are not public on Owner.
Non-Goals
Embedding is not:
- class inheritance
- subclassing
- trait implementation reuse
- interface default methods
- mixins
- extension methods
- implicit conversion
- automatic delegation to private implementation details
Class inheritance is not part of the core language direction. Interface default methods and trait-style code reuse are not part of the core language direction.