Interfaces

Interfaces

Interfaces define contracts that classes can implement.

Basic Interface

interface Printable {
  method print() void;
}
 
interface HasValue {
  var value: i32;
}
 
class Counter implements Printable, HasValue {
  init(v: i32) {
    this.value = v;
  }
 
  method print() void {
    println(this.value);
  }
}

Extending an Interface

An interface can extend one parent. It inherits the parent's fields, required methods, and default implementations. Inheritance can span several levels, and the parent may be declared later in the same module.

/** Provides a readable value. */
interface Readable {
    /** Returns the current value. */
    method read() i32;
}
 
/** Adds a write operation to the readable contract. */
interface Writable extends Readable {
    /** Replaces the current value. */
    method write(value: i32) void;
}

A class implementing Writable must provide both methods and also satisfies Readable, including generic constraints requiring Readable. Parents may be qualified or generic, such as interface Child<T> extends api.Parent<T>. Lifetime arguments use the usual annotation syntax.

A child may replace an inherited method with the same signature, visibility, const/unsafe qualifiers, generic constraints, and lifetime contract. A body provides a new default; a declaration without a body requires implementing classes to supply the method. The nearest default wins, and a class method takes precedence over every default. Calls through a parent use the same selected implementation. Inherited field names cannot be redeclared.

Interfaces do not own values. Use ref Interface or const ref Interface to borrow an existing concrete object. Creating a view or converting a child view to a parent view never allocates on the heap or copies the object. The concrete owner remains responsible for destruction, and a view cannot outlive it. A borrowed interface allows method calls but cannot replace the concrete object through assignment. Conversion from parent to child is not supported.

Bare interface types cannot be used for variables, fields, parameters, ordinary returns, or owned container elements. Store a concrete type, use a generic type parameter constrained by the interface, or store a borrowed view with a suitable lifetime.

Inheritance preserves member visibility: private members remain accessible only within their defining module. Multiple parents and inheritance cycles are rejected. A class may still implement several unrelated interfaces, but conflicting defaults require an explicit class implementation.

Borrowed views share one static dispatch table per concrete class and interface. The table contains method pointers and a parent table pointer when the interface extends another interface. Views have no destruction slot.

Rebuild existing .moon libraries: owning interface values have been removed, and the interface dispatch-table layout has changed.

Visibility

Interfaces and their members are private by default and are marked public individually. A private interface member is only callable through the interface from inside the interface's module; a class implementing a public member must declare its implementation public. Interface fields inherited by a class keep the visibility they were declared with on the interface.

public module shapes {
    public interface IShape {
        public method area() f64;
        method debug_id() i32 { return 0; }   // module-internal default
    }
    public class Square implements IShape {
        var side: f64;
        init(s: f64) { this.side = s; }
        public method area() f64 { return this.side * this.side; }
    }
}

Const Methods

An interface member declared const method promises not to change the object, so it can be called through a const ref to the interface. Every implementation must then be a const method too (see Const Methods). A class may declare extra const methods the interface does not require.

interface IShape {
    const method area() f64;
}
function measure(s: const ref IShape) f64 { return s.area(); }

Methods That Return Errors

An interface method declares its result enum as its return type, including methods with default implementations:

/** Describes an operation that may fail. */
interface Operation {
    /** Runs the operation or returns an owned system error. */
    method run() SystemResult<void>;
}
 
/** Propagates an error from interface dispatch. */
function invoke(operation: ref Operation) SystemResult<void> {
    try operation.run();
    return SystemResult.Ok;
}

Implementations and inherited overrides must preserve the method's result type. An implementation that always succeeds still explicitly returns the success variant. Callers may match the result, propagate it with try, or explicitly discard it with ignore_error. See Error Handling.

Interface Fields

Interfaces can declare fields that implementing classes must have:

interface Named {
    var name: i32;  // Implementing class must have this field
}
 
class Person implements Named {
    // 'name' field is inherited from Named interface
    
    init(n: i32) {
        this.name = n;
    }
}

A class can own a concrete implementation through a generic parameter:

/** Handles an input value. */
interface Handler {
    /** Transforms the input. */
    method handle(value: i32) i32;
}
 
/** Doubles each input. */
class Doubler implements Handler {
    /** Creates the handler. */
    init() {}
    /** Doubles the input. */
    method handle(value: i32) i32 { return value * 2; }
}
 
/** Owns its concrete handler directly. */
class Service<H: Handler> {
    var handler: H;
    /** Moves the concrete handler into the service. */
    init(handler: H) { this.handler = handler; }
    /** Handles one input. */
    method run(value: i32) i32 { return this.handler.handle(value); }
}

Service<Doubler>(Doubler()) stores the handler directly and destroys it when the service is dropped. Code needing dynamic dispatch can borrow the concrete handler as ref Handler.

Interface Return Requirements

A bodyless interface method may name an interface as its return requirement. Each implementation must return a concrete class implementing that interface. This is a compile-time contract used by generic code, including IIterable.iter(). Such an implementation cannot be converted to a borrowed view of that interface for dynamic dispatch. An ordinary function or a default method body cannot return an owning interface value.

Default Implementations

Interfaces can provide default method implementations:

interface Answerable {
  method answer() i32 {
    return 42;
  }
}
 
class Thinker implements Answerable {
  init() {}
}
 
function main() i32 {
    var t = Thinker();
    return t.answer();  // Uses default: returns 42
}

Classes can override default implementations:

class DeepThinker implements Answerable {
    init() {}
 
    method answer() i32 {
        return 43;  // Override default
    }
}

Generic Interfaces

Interfaces can have type parameters, allowing generic contracts:

interface Container<T> {
    method get() T;
    method set(value: T) void;
}
 
class Box<T> implements Container<T> {
    var value: T;
 
    init(v: T) {
        this.value = v;
    }
 
    method get() T {
        return this.value;
    }
 
    method set(v: T) void {
        this.value = v;
    }
}

Implementing Generic Interfaces with Concrete Types

A non-generic class can implement a generic interface with a specific type:

interface Wrapper<T> {
    method unwrap() T;
}
 
class IntWrapper implements Wrapper<i32> {
    var value: i32;
 
    init(v: i32) {
        this.value = v;
    }
 
    method unwrap() i32 {
        return this.value;
    }
}

Generic Class Implementing Generic Interface

When a generic class implements a generic interface, the type parameters flow through:

interface Mappable<T, U> {
    method map(f: fn(T) U) U;
}
 
class Value<T> implements Mappable<T, T> {
    var data: T;
 
    init(d: T) {
        this.data = d;
    }
 
    method map(f: (T) => T) T {
        return f(this.data);
    }
}

Lifetimes in Interface Methods

An interface method may tie a parameter to the receiver with the builtin 'this lifetime, exactly as a class method does — and the tie is part of the contract. An implementing class must repeat the interface's lifetime names verbatim; dropping or renaming one is a conformance error, since a caller dispatching through the interface sees only the interface's signature:

interface ISink {
    public method accept(cb: <'this>(i32) => i32) void;
}
 
class Holder implements ISink {
    var cb: <'_>(i32) => i32;
    /* must say <'this> here too - a plain <'_> is rejected */
    public method accept(cb: <'this>(i32) => i32) void { this.cb = cb; }
}

Call sites are then checked against the implementing object's real scope, the same as for classes (see Memory Safety).

Multiple Interfaces

Classes can implement multiple interfaces:

interface Runnable {
    method run() void;
}
 
interface Stoppable {
    method stop() void;
}
 
class Service implements Runnable, Stoppable {
    var running: bool;
 
    init() {
        this.running = false;
    }
 
    method run() void {
        this.running = true;
    }
 
    method stop() void {
        this.running = false;
    }
}

Builtin and Stdlib Interfaces

IError (error handling, code() and message()) is builtin and cannot be redefined; see Builtin Types. catch (error: ref IError) borrows a runtime-owned exception. Catch bindings must explicitly use ref or const ref; they never own the error. Exception transport still allocates and is separate from allocation-free interface views.

The iteration protocol comes from the standard library (iterator.sun, see Iteration):

  • IIterator<T, Container> - next(c: ref Container) Option<T>; None ends the sequence
  • IIterable<T, Self> - iter() returning an iterator
using std;
 
// Implementing IIterator (self-iterating pattern)
class NumberIterator implements IIterator<i32, NumberIterator> {
    var current: i32;
    var max: i32;
 
    init(start: i32, end: i32) {
        this.current = start;
        this.max = end;
    }
 
    method next(self: ref NumberIterator) Option<i32> {
        if (this.current >= this.max) {
            return Option.None;
        }
        var result = this.current;
        this.current = this.current + 1;
        return Option.Some(result);
    }
}

Classes can implement multiple interfaces by separating them with commas.

⚠️

Attempting to redefine the builtin IError interface will result in a compilation error.