Luce Base
LuceSourceLuciaOS

14. Interfaces

14.1 Declaration and conformance#

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

pub struct FileWriter: Writer:
    var descriptor: i32

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

An interface is a nominal set of method requirements. Conformance is declared on the type, struct Name: Interface:, with several interfaces separated by commas, never retroactively. Every requirement is supplied with the exact signature by a method, never a static function (§9.5); 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#

from io import Writer

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

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

A requirement says nothing about the receiver; the conformer's implementation does (§9.5). A generic body may call a requirement on a value it may not change, a parameter or what a const T* reaches, as largest[T: Comparable](left: T, right: T) calls left.compare(right); an instantiation whose argument implements that requirement by changing its receiver is refused where it is written, the generics it passes the parameter on to included. A generic type whose method calls a changing requirement on a part of its receiver holds the conformer through a pointer, var sink: W*.

14.3 Interface views#

from io import Writer

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 none of T's methods implementing the interface's requirements changes its receiver (§9.5). A conformer whose implementations change it is therefore addressable as var to be viewed; what the view may do is fixed when it is formed, and a call through it needs nothing more.
  • 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 requires no allocation to create, and nominal explicit conformance means every table is known at compile time.

14.4 Standard protocols#

Equatable and Hashable are the compiler-known marker interfaces behind == and value.hash(); they are derived structurally (§7.4) and cannot be implemented by hand. Comparable is implemented with compare(other) -> i64, negative, zero, or positive; the compiler supplies it for integers, floats (IEEE order, with NaN unordered and compare trapping on it), char, and str.

Iterable[T, I] and Iterator[T] are what for consumes (§8.3): interface Iterable[T, I: Iterator[T]]: func iterator() -> I, and interface Iterator[T]: func next() -> T?. The iterator type is a parameter, so for resolves it statically.

Display writes a value to a sink: func display(sink: Writer) -> !. The compiler supplies it for integers (decimal), floats (the shortest decimal that reads back as the value, the closest of those: decimal notation for a decimal exponent from -6 to 14 and 1.5e-7, 1e15 otherwise; digits without a point keep a .0, -5.0, -0.0; inf, -inf, nan; an f32 shows its own shortest digits, 0.1 for 0.1f32, and an f16 those of its value as an f32), bool, char, str, and pointers (hexadecimal with 0x). Formatted strings call it for each field; a field with a format specification (§5.5) lays out a number or text instead.

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 the caller's sink, which is also how C's snprintf chain works, minus the format string.