Interfaces and structs
dusk’s object-oriented surface is deliberately small. Structs hold data, impl blocks attach methods, and interfaces provide polymorphism through vtable dispatch. There are no classes and no inheritance of any kind. Interface declarations are gated behind @paradigm oop; structs and methods are available in every paradigm. See Paradigm system for how the gates work.
Structs
Section titled “Structs”A struct is a plain data container. Structs are paradigm agnostic: they are not gated by @paradigm oop and can be declared and used in any file.
struct Point { x: float64, y: float64,}A struct value is built with a literal that names every field:
p := Point { x: 1.0, y: 2.0 }The checker validates struct literals: an unknown field name, a duplicated field, or a missing field is a compile error (since 0.2.5). Fields are read with dot syntax, p.x. Writing a field, p.x = v, requires the root binding to be declared mut, the same immutability rule that governs every projection (since 0.2.6).
Structs are passed by value, like everything else in dusk. Passing a struct to a function copies it; the callee’s changes do not reach the caller’s copy. To share one struct, allocate it and pass the pointer. See Memory.
Methods and impl blocks
Section titled “Methods and impl blocks”Methods are attached to a struct with an impl block. Inside a method, self names the receiver.
@paradigm procedural
struct Counter { n: int64,}
impl Counter { func get() -> int64 { return self.n }
func add(k: int64) -> void { self.n = self.n + k }}
func main() -> int32 { mut c := Counter { n: 3 } c.add(4) println(c.get()) println(c.n) return 0}This prints 7 twice: methods take the receiver by pointer, so an assignment through self updates the caller’s value in place. This is why a stateful allocator’s bump offset advances when it is passed through using. See Memory.
A method can also be called directly through a managed pointer to the struct. c.get() on a c: *Counter dispatches after the same generation check an explicit dereference runs, so calling a method on a freed receiver faults instead of reading stale memory.
Interfaces
Section titled “Interfaces”Interfaces require @paradigm oop in the declaring file. The interface is the only OOP construct in the language.
interface DisplayName { getName() -> string setName(name: string) -> void}Four rules govern interfaces:
- An interface defines a contract of method signatures.
- A struct satisfies an interface by an explicit
impldeclaration. There is no structural or implicit conformance. - There is no inheritance. One interface cannot extend another.
- Composition is the only way to combine behaviors.
An impl for an interface names both the interface and the struct:
impl DisplayName for Person { func getName() -> string { return self.name } func setName(name: string) -> void { self.name = name }}The checker enforces the contract:
- An impl must provide every method of the interface; an incomplete impl is rejected (since 0.2.5).
- A duplicate
impl I for Tis a checker error (since 0.2.6). - An
implblock on a generic type is diagnosed rather than silently dropped (since 0.2.6).
Interface values and dispatch
Section titled “Interface values and dispatch”A function parameter typed as an interface accepts any struct with the matching impl. Conformance is checked wherever a struct meets an interface type: at call sites (since 0.2.5), and at bindings and returns (since 0.2.6). A missing impl is a checker error, never an undefined symbol at link time.
@paradigm oop
interface Speaker { speak() -> int64}
struct Dog { sound: int64,}
struct Cat { sound: int64,}
impl Speaker for Dog { func speak() -> int64 { return self.sound }}
impl Speaker for Cat { func speak() -> int64 { return self.sound }}
func describe(s: Speaker) -> int64 { return s.speak()}
func main() -> int32 { d := Dog { sound: 70 } c := Cat { sound: 99 } println(describe(d)) println(describe(c)) return 0}Dispatch is static when the receiver’s concrete type is known at the call, which is the common case and is zero cost. It falls back to a vtable call only when the type is erased behind the interface, as in describe above. The Allocator interface behind alloc and free works the same way. See Memory.
Interface values do not cross threads. A spawn or submit capture or a channel element containing an interface value, wherever it sits, including buried in a struct or enum field, is a compile error, since the value may view the spawning frame. See Concurrency.
No inheritance
Section titled “No inheritance”There is no inheritance of any kind, not for structs and not for interfaces. A struct cannot extend a struct, and an interface cannot extend an interface. Code reuse happens through composition only: embed a struct as a field, or implement multiple interfaces on one struct.
The Display interface and printing
Section titled “The Display interface and printing”Any type that implements the Display interface can be passed to print and println:
interface Display { toString() -> string}A struct with a Display impl prints through its toString. The rules are strict (since 0.2.6):
- Passing a struct with no
Displayimpl to a print builtin is a compile error. - Printing an enum, a slice, a tuple, or a pointer is also a compile error.
- Print never emits silence for a value it cannot render.
@paradigm oop
interface Display { toString() -> string}
struct Point { x: int64, y: int64,}
impl Display for Point { func toString() -> string { return "Point" }}
func main() -> int32 { p := Point { x: 1, y: 2 } println(p) return 0}This prints Point. The Display interface is declared in the program, as above, and the print builtins key on the impl Display for T providing toString() -> string. Primitive types have built-in printers and need no impl. See Builtins for the full printing rules and format strings.