5.1.43. Interfaces

This tutorial covers interface-based polymorphism using the daslib/interfaces module. Interfaces let you define abstract method contracts that structs can implement, enabling polymorphic dispatch without class inheritance.

Prerequisites: Dynamic Type Checking (Tutorial 39).

require daslib/interfaces

5.1.43.1. Defining an interface

Use [interface] on a class to declare it as an interface. An interface may contain only function-typed fields — abstract methods or default implementations:

[interface]
class IGreeter {
    def abstract greet(name : string) : string
}

5.1.43.2. Implementing an interface

Use [implements(IFoo)] on a struct to generate the proxy class and getter. Implement each method with the InterfaceName`method naming convention:

[implements(IGreeter)]
class FriendlyGreeter {
    def FriendlyGreeter() {
        pass
    }
    def IGreeter`greet(name : string) : string {
        return "Hey, {name}!"
    }
}

5.1.43.3. is / as / ?as operators

The InterfaceAsIs variant macro enables three operators that work with interface types:

var g = new FriendlyGreeter()

// is — compile-time check (zero runtime cost)
print("{g is IGreeter}\n")         // true

// as — returns the interface proxy
var iface = g as IGreeter
iface->greet("Alice")

// ?as — null-safe: returns null when the pointer is null
var nothing : FriendlyGreeter?
var safe = nothing ?as IGreeter    // null

is is resolved entirely at compile time — the result is baked into the program as a boolean constant.

5.1.43.4. Multiple interfaces

A struct can implement any number of interfaces by listing multiple [implements(...)] annotations:

[interface]
class IDrawable {
    def abstract draw(x, y : int) : void
}

[interface]
class ISerializable {
    def abstract serialize : string
}

[implements(IDrawable), implements(ISerializable)]
class Sprite {
    name : string
    def Sprite(n : string) { name = n }
    def IDrawable`draw(x, y : int) {
        print("Sprite \"{name}\" at ({x},{y})\n")
    }
    def ISerializable`serialize() : string {
        return "sprite:{name}"
    }
}

5.1.43.5. Polymorphic dispatch

Interfaces enable true polymorphic dispatch — pass interface proxies to functions that accept the interface type:

def draw_all(var objects : array<IDrawable?>) {
    for (obj in objects) {
        obj->draw(0, 0)
    }
}

var drawables : array<IDrawable?>
drawables |> push(circle as IDrawable)
drawables |> push(sprite as IDrawable)
draw_all(drawables)

5.1.43.6. Interface inheritance

Interfaces can extend other interfaces using normal class inheritance syntax. A struct that implements a derived interface automatically supports is/as/?as for all ancestor interfaces:

[interface]
class IAnimal {
    def abstract name : string
    def abstract sound : string
}

[interface]
class IPet : IAnimal {
    def abstract owner : string
}

[implements(IPet)]
class Dog {
    dog_name : string
    owner_name : string
    def Dog(n, o : string) { dog_name = n; owner_name = o }
    def IPet`name() : string { return dog_name }
    def IPet`sound() : string { return "Woof" }
    def IPet`owner() : string { return owner_name }
}

Now Dog supports both IPet and IAnimal:

var d = new Dog("Rex", "Alice")
print("{d is IPet}\n")       // true
print("{d is IAnimal}\n")    // true

var animal = d as IAnimal
animal->name()               // "Rex"
animal->sound()              // "Woof"

5.1.43.7. Default method implementations

Non-abstract methods in an interface class provide default implementations. The implementing struct only needs to override the abstract methods — defaults are inherited by the proxy:

[interface]
class ILogger {
    def abstract log_name : string
    // Default — optional to override
    def format(message : string) : string {
        return "[{self->log_name()}] {message}"
    }
}

[implements(ILogger)]
class SimpleLogger {
    def SimpleLogger() { pass }
    def ILogger`log_name() : string { return "simple" }
    // format() uses ILogger's default
}

A struct that overrides the default provides its own implementation:

[implements(ILogger)]
class FancyLogger {
    def FancyLogger() { pass }
    def ILogger`log_name() : string { return "fancy" }
    def ILogger`format(message : string) : string {
        return "*** [fancy] {message} ***"
    }
}

5.1.43.8. Completeness checking

If an implementing struct is missing an abstract method, the compiler reports an error at compile time:

error[30111]: Foo does not implement IBar.method

Methods with default implementations are optional — the proxy inherits the default from the interface class. Only abstract methods (def abstract) are required.

5.1.43.9. Quick reference

Syntax

Description

[interface]

Mark a class as an interface

[implements(IFoo)]

Generate proxy and getter for IFoo

def abstract method(...) : T

Declare an abstract method (required)

def method(...) : T { ... }

Declare a default method (optional to override)

def IFoo`method(...)

Implement (or override) a method on a struct

ptr is IFoo

Compile-time check (true/false)

ptr as IFoo

Get the interface proxy

ptr ?as IFoo

Null-safe proxy access

class IChild : IParent

Interface inheritance

See also

Dynamic Type Checkingis/as on class hierarchies (Tutorial 39).

Variant Macros — how the InterfaceAsIs macro works internally (Macro Tutorial 8).

Interfaces module — standard library reference.

Full source: tutorials/language/43_interfaces.das

Previous tutorial: Testing Tools (faker + fuzzer)

Next tutorial: Compiling and Running Programs at Runtime