-
Notifications
You must be signed in to change notification settings - Fork 4
Interfaces
Interfaces define a set of method signatures that a type must satisfy. They are zero-cost conformance contracts no vtable, no runtime dispatch, no storage overhead. A type conforms to an interface if it has all the required methods with matching signatures, whether or not it explicitly declares conformance.
Interfaces are a compile-time mechanism. For runtime polymorphism, use a base
class with virtual methods and dispatch through a base pointer. Interfaces
and virtual dispatch solve different problems and can be used together on the same type.
interface Serializable {
fn serialize(const self) -> [byte]
fn byte_size(const self) -> i32
}
An interface body contains method signatures, static function signatures, and operator signatures. No
method bodies, no variables, no constants, no nested types. Every member is implicitly pub the
priv and prot modifiers are compile errors on interface members. Explicit pub is permitted but
has no effect.
A type declares interface conformance with impl on a class or through
extend ... impl on a struct, enum, or class. The
compiler verifies at declaration time that all required methods are present with matching signatures:
class Document impl Serializable {
var content: string
fn Document(self, content: string) {
self.content = content
}
fn serialize(const self) -> [byte] {
return self.content.to_bytes()
}
fn byte_size(const self) -> i32 {
return self.content.length()
}
}
Note
Conformance is structural, not nominal. A type that has all the required methods with matching signatures satisfies the interface implicitly. The check happens at the point of use. See Structural conformance section below for details.
For structs and enums, conformance goes through extend:
struct Point {
var x: f64
var y: f64
}
extend Point impl Serializable {
fn serialize(const self) -> [byte] {
// serialization logic
}
fn byte_size(const self) -> i32 {
return 16 // two f64s
}
}
See Extends for the full extend system.
Explicit impl is not required. A type that has all the required methods with matching signatures
satisfies the interface implicitly. The check happens at the point of use:
class Logger {
fn serialize(const self) -> [byte] { ... }
fn byte_size(const self) -> i32 { ... }
}
// Logger never mentions Serializable, but satisfies it structurally
fn <T impl Serializable> write_to_disk(data: T) { ... }
write_to_disk(Logger()) // compiles: Logger has serialize() and byte_size()
The difference between explicit and implicit conformance is when the check happens at declaration
time (class Foo impl Bar) or at first use (fn <T impl Bar>). The runtime behavior is identical.
Interfaces can declare type parameters:
interface <T> Container {
fn add(self, item: T)
fn get(const self, index: i32) -> T
fn size(const self) -> i32
}
class IntBuffer impl Container<i32> {
var data: [i32]
fn IntBuffer(self) { self.data = [] }
fn add(self, item: i32) {
self.data.push(item)
}
fn get(const self, index: i32) -> i32 {
return self.data[index]
}
fn size(const self) -> i32 {
return self.data.length()
}
}
Type parameters on interfaces can have constraints:
interface <T impl Comparable, U derives Base> Registry {
fn lookup(const self, key: T) -> U
fn store(self, key: T, value: U)
}
Generic defaults are not permitted on interface type parameters.
See Bounds for the full constraint system.
Interfaces can require operator overloads:
interface Equatable {
fn op ==(const self, other: Self) -> bool
}
interface Convertible {
fn op as(self) -> string
}
Self in an interface signature refers to the conforming type. A class that impl Equatable must
provide fn op ==(const self, other: Self) -> bool where Self resolves to that class.
See Operators for the full list of overloadable operators.
Interfaces can require constructor signatures:
interface Defaultable {
fn Defaultable(self)
}
interface <T> Cloneable {
fn Cloneable(self, source: T)
}
A conforming type must have a constructor that matches the parameter list. The constructor name in the implementation uses the type's own name, not the interface name:
class Config impl Defaultable {
var timeout: i32
fn Config(self) { // satisfies Defaultable's constructor requirement
self.timeout = 30
}
}
Constructor requirements can carry @copy or @move attributes to require a specific lifecycle
category. This is how you express "T must be copyable" or "T must be movable" as an interface bound:
interface Copyable {
fn Copyable(self) // default ctor
@copy
fn Copyable(self, other: Self)
}
interface Moveable {
fn Moveable(self)
@move
fn Moveable(self, other: Self)
}
fn <T impl Copyable> store(item: T) -> T { ... }
fn <T impl Moveable> consume(item: T) { ... }
The standard library will provide canonical lifecycle interfaces; these are just examples. See Lifecycle Categories for the underlying model.
Interfaces can require static methods:
interface <T> Parseable {
static fn from_string(input: string) -> T
static fn from_bytes(data: [byte]) -> T
}
class Timestamp impl Parseable<Timestamp> {
var epoch: i64
fn Timestamp(self, epoch: i64) {
self.epoch = epoch
}
static fn from_string(input: string) -> Timestamp {
// parsing logic
return Timestamp(0)
}
static fn from_bytes(data: [byte]) -> Timestamp {
// parsing logic
return Timestamp(0)
}
}
Interface methods can use Self as a return type. Self resolves to the conforming type a class
that impl Chainable and returns Self returns its own type:
interface Chainable {
fn then(self, next: Self) -> Self
}
class Pipeline impl Chainable {
fn then(self, next: Self) -> Self {
// Self resolves to Pipeline
return self
}
}
Kairo has no user-visible reference types AMT infers reference semantics where
needed. In practice this means Self returns enable fluent chaining without -> or explicit
dereferencing:
Pipeline().then(other).then(another).run()
To return a pointer to the conforming type, use *Self:
interface Cloneable {
fn clone(self) -> *Self
}
Interfaces can inherit from other interfaces with derives. A type that conforms to the derived
interface must satisfy all inherited interfaces as well:
interface Readable {
fn read(self, buffer: [byte], count: i32) -> i32
}
interface Seekable {
fn seek(self, position: i64)
fn tell(const self) -> i64
}
interface Stream derives Readable, Seekable {
fn close(self)
fn flush(self)
}
A class that impl Stream must provide read, seek, tell, close, and flush:
class FileStream impl Stream {
var fd: i32
fn FileStream(self, path: string) { ... }
fn read(self, buffer: [byte], count: i32) -> i32 { ... }
fn seek(self, position: i64) { ... }
fn tell(const self) -> i64 { ... }
fn close(self) { ... }
fn flush(self) { ... }
}
Interface inheritance is purely additive the derived interface's requirements are the union of its own methods and all inherited methods. There is no diamond problem because interfaces carry no implementation or state.
Multiple inheritance is permitted:
interface Loggable derives Serializable, Printable {
fn log_level(const self) -> i32
}
The primary use of interfaces is constraining generic type parameters with impl:
fn <T impl Serializable> save(data: T, path: string) {
var bytes = data.serialize()
write_file(path, bytes)
}
impl checks structural conformance the type satisfies the interface's required method signatures.
This is distinct from derives, which checks class inheritance. See Bounds
for the full constraint system.
fn <T impl Serializable> save(data: T) { ... } // T has serialize() and byte_size()
fn <T derives Base> process(data: T) { ... } // T is a subclass of Base
Interfaces must be declared at module scope. They cannot be nested inside classes, structs, enums, or other interfaces:
interface Valid {
fn check(const self) -> bool
}
class Outer {
// interface Invalid { ... } // compile error: interfaces cannot be nested
}
See Modules for module organization.
Interface methods have no bodies. Every method is a requirement that the conforming type must fulfill:
interface Hashable {
fn hash(const self) -> u64
// fn hash(const self) -> u64 { return 0 } // compile error: no default implementations
}
This keeps interfaces as zero-cost contracts. There is no method resolution order, no inheritance
of behavior, and no hidden dispatch. If you need shared implementation, use a base
class with derives.
Interfaces cannot declare variables, constants, static variables, or eval bindings:
interface Invalid {
// var x: i32 // compile error
// const Y: i32 = 10 // compile error
// static z: i32 = 0 // compile error
fn valid_method(self) // ok
}
// Basic interface
interface Drawable {
fn draw(const self) -> string
fn bounds(const self) -> (f64, f64, f64, f64)
}
// Generic interface with constraints
interface <T impl Comparable> SortedContainer {
fn insert(self, item: T)
fn min(const self) -> T
fn max(const self) -> T
}
// Interface with operators
interface Arithmetic {
fn op +(self, other: Self) -> Self
fn op -(self, other: Self) -> Self
fn op ==(const self, other: Self) -> bool
}
// Interface inheritance
interface Flushable {
fn flush(self)
}
interface BufferedWriter derives Flushable {
fn write(self, data: [byte])
fn buffer_size(const self) -> i32
}
// Class conformance
class Renderer impl Drawable {
fn draw(const self) -> string { return "rendering" }
fn bounds(const self) -> (f64, f64, f64, f64) { return (0.0, 0.0, 100.0, 100.0) }
}
// Struct conformance via extend
extend Point impl Drawable {
fn draw(const self) -> string { return f"({self.x}, {self.y})" }
fn bounds(const self) -> (f64, f64, f64, f64) { return (self.x, self.y, self.x, self.y) }
}
// Generic bound
fn <T impl Drawable> render_all(items: [T]) {
for item in items {
std::println(item.draw())
}
}
This wiki mirrors the language reference at kairolang.org/docs. To edit a page, edit the source at kairo-web/src/content/docs/language changes sync automatically.
Start here: Primitives
1. Fundamentals
2. Functions & Control Flow
3. Types
4. Modules & Metaprogramming
5. Memory & Safety
6. Interop & Concurrency