Luce Base
LuceEngineeringLuciaOS

14. Interfaces

14.1 Declaration and conformance#

pub interface Writer:
    mutating func write(self, bytes: const u8[]) -> usize!

pub struct FileWriter implements Writer:
    var descriptor: c.int

    pub mutating func write(self, bytes: const u8[]) -> usize!:
        ...

An interface is a nominal set of method requirements. Conformance is declared with implements on the type, never retroactively. Every requirement is supplied with the exact signature; an implementation may be non-fallible where the requirement is fallible. Interfaces do not inherit, have no default bodies, no fields, no constructors, and no generic methods.

14.2 Static use#

func write_header[W: Writer](writer: W*, header: Header) -> unit!:
    discard(try writer.write(header.bytes()))

A constrained generic is statically dispatched and monomorphised. Nothing allocates and nothing goes through a table.

14.3 Interface views#

var file = FileWriter(descriptor = 1)
let writer: Writer = &file
try writer.write(data)

Using an interface as a type denotes a two-word value, a pointer to the conforming object and a pointer to its witness table, that borrows the object and does not own it.

  • A view is formed from T* where T implements the interface, or from const T* when the interface has no mutating requirement. A mutating requirement therefore needs the conformer addressable as var; mutability is fixed when the view is formed.
  • Calls dispatch through the table: one indirect call. The table is static data, emitted once per type-and-interface pair.
  • The view is copyable and carries no lifetime; the object must outlive every view of it, the same obligation as for any pointer.
  • Writer? uses the null niche on the data word.
  • There is no boxing, no equality, and no downcast. Code that needs to recover the concrete type should use an enum.

Why a view. Full Luce's interface value boxes the concrete value and gives it copy semantics, which needs an owner. Without reference counting, the two-word borrowed form of Rust's dyn Trait, Zig's std.mem.Allocator, and Go's interface is the only representation that costs nothing to create, and nominal explicit conformance means every table is known at compile time.

14.4 Standard protocols#

Equatable, Hashable, and Comparable are the compiler-known marker interfaces behind ==, hash, and ordered algorithms; the first two are derived structurally and cannot be implemented by hand, the third is implemented with compare(self, other) -> i64.

Iterable[T] and Iterator[T] are what for consumes (§8.3): func iterator(self) -> I where I is a value type with mutating func next(self) -> T?.

Display writes a value to a sink: func display(self, sink: Writer) -> unit!. The compiler supplies it for scalars, bool, char, and str. Formatted strings call it for each field.

Writer (§14.1) is the standard sink. io.stdout() and io.stderr() answer one. print(text) and print(f"...") write to standard output with a newline and ignore a failed write.

Why Display takes a sink. Full Luce's display returns an owned string. In Base there is no owner for it, so the value writes itself to whoever asked, which is also how C's snprintf chain works, minus the format string.