Extends

Extends

extend blocks add methods, operators, and static functions to types declared elsewhere. They are the primary way to attach behavior to structs and enums, which cannot contain methods in their body. Classes can also be extended.


Basic Syntax

struct Point {
    var x: f64
    var y: f64
}

extend Point {
    fn length(self) const -> f64 {
        return std::sqrt(self.x * self.x + self.y * self.y)
    }

    fn op +(self, other: Point) -> Point {
        return Point { x: self.x + other.x, y: self.y + other.y }
    }

    static fn origin() -> Point {
        return Point { x: 0.0, y: 0.0 }
    }
}

var p = Point { x: 3.0, y: 4.0 }
p.length()       // 5.0
Point::origin()  // Point { x: 0.0, y: 0.0 }

Methods added via extend are called the same way as methods defined in a class body there is no syntactic distinction at the call site.


What Can Be Extended

TypeSupports extend
StructsYes the only way to add methods
EnumsYes the only way to add methods
ClassesYes
UnionsNo
InterfacesNo

What Extends Can Add

The rules differ by type:

Structs

AllowedNot allowed
MethodsConstructors
Static functionsDestructors (fn op delete)
Arithmetic / comparison operatorsCopy / move assignment (fn op =)
fn op as (type conversion)
fn op in (iteration)

Structs are trivially copyable. Extending lifecycle operations (destructors, copy/move) would break that guarantee. See Structures.

Enums

AllowedNot allowed
MethodsConstructors
Static functionsDestructors
Comparison / arithmetic operatorsCopy / move assignment
fn op as (type conversion)

Classes

AllowedNot allowed
MethodsConstructors
Static functionsDestructors
All operatorsCopy / move assignment

Classes already support methods and operators in their body. extend on a class is useful for separating interface conformance or adding functionality in a different section of the codebase (within the same file).

Note

Constructors, destructors, and copy/move assignment cannot be added via extend on any type. If you need construction logic on a struct, extend a static factory function instead. If you need a destructor, use a class.


Interface Conformance

extend ... impl declares that a type satisfies an interface and provides the required methods:

interface Drawable {
    fn draw(self) const -> string
}

extend Point impl Drawable {
    fn draw(self) const -> string {
        return f"({self.x}, {self.y})"
    }
}

The compiler verifies at the extend declaration that all interface requirements are satisfied. Missing methods are a compile error.

A type can conform to multiple interfaces through separate extend blocks:

extend Point impl Drawable {
    fn draw(self) const -> string { ... }
}

extend Point impl Serializable {
    fn serialize(self) const -> [byte] { ... }
    fn byte_size(self) const -> i32 { ... }
}

See Interfaces for interface declarations and structural conformance.


Generic Extends

When extending a generic type, redeclare the type parameters:

struct <T> Pair {
    var first: T
    var second: T
}

extend <T> Pair<T> {
    fn swap(self) -> Pair<T> {
        return Pair<T> { first: self.second, second: self.first }
    }
}

The type parameters in the extend block must match the original declaration. Constraints can be added via impl, derives, or where clauses:

extend <T impl Comparable> Pair<T> {
    fn max(self) const -> T {
        return if self.first > self.second { self.first } else { self.second }
    }
}

This max method is only available on Pair<T> when T satisfies Comparable. Calling Pair<SomeNonComparable>.max() is a compile error.

Generic interface conformance

interface <T> Container {
    fn size(self) const -> i32
    fn get(self, index: i32) const -> T
}

extend <T> Pair<T> impl Container<T> {
    fn size(self) const -> i32 { return 2 }

    fn get(self, index: i32) const -> T {
        return if index == 0 { self.first } else { self.second }
    }
}

See Bounds for the full constraint system.


Visibility

Extended methods can have pub, prot, or priv visibility:

extend Point {
    pub fn distance(self, other: Point) const -> f64 {
        return (self - other).length()
    }

    priv fn validate(self) const -> bool {
        return self.x >= 0.0 && self.y >= 0.0
    }
}

The default visibility for extended methods matches the type’s convention pub for struct and enum extensions, pub for class method extensions.


Self in Extend Blocks

Self is available in extend blocks and resolves to the extended type:

extend <T> Pair<T> {
    fn duplicate(self) const -> Self {
        // Self resolves to Pair<T>
        return Pair<T> { first: self.first, second: self.second }
    }
}

Same-File Restriction

A plain extend block must be in the same file as the type definition:

// point.k
struct Point { var x: f64; var y: f64 }

extend Point {
    fn length(self) const -> f64 { ... }   // ok: same file as Point
}
// other.k
import point::Point

// extend Point { ... }   // compile error: Point is defined in a different file

For extend ... impl blocks (interface conformance), the rule is looser: either the type or the interface must be defined in the same file as the extend block:

// drawable.k
import point::Point

interface Drawable {
    fn draw(self) const -> string
}

// This is legal even though Point is defined in point.k,
// because Drawable is defined here:
extend Point impl Drawable {
    fn draw(self) const -> string {
        return f"({self.x}, {self.y})"
    }
}

This prevents conflicts between extensions in different files while still allowing interface conformance to be declared where the interface is defined.


Multiple Extend Blocks

A type can have multiple extend blocks in the same file. Each block can target different interfaces or group related functionality:

struct Color {
    var r: u8
    var g: u8
    var b: u8
}

// Arithmetic operations
extend Color {
    fn op +(self, other: Color) -> Color {
        return Color {
            r: (self.r as i32 + other.r as i32).min(255) as u8,
            g: (self.g as i32 + other.g as i32).min(255) as u8,
            b: (self.b as i32 + other.b as i32).min(255) as u8,
        }
    }
}

// Serialization
extend Color impl Serializable {
    fn serialize(self) const -> [byte] { ... }
    fn byte_size(self) const -> i32 { return 3 }
}

// Display
extend Color impl Drawable {
    fn draw(self) const -> string {
        return f"rgb({self.r}, {self.g}, {self.b})"
    }
}

Extend vs Class Methods

For classes, there is no semantic difference between a method in the class body and a method in an extend block both produce the same compiled output. The choice is organizational:

class Server {
    var port: i32

    fn Server(self, port: i32) { self.port = port }

    // Core functionality in the class body
    fn start(self) { ... }
    fn stop(self) { ... }
}

// Interface conformance separated
extend Server impl Loggable {
    fn to_log_string(self) const -> string {
        return f"Server(:{self.port})"
    }
}

Summary

// Extend a struct with methods
struct Vec2 { var x: f64; var y: f64 }

extend Vec2 {
    fn length(self) const -> f64 = std::sqrt(self.x * self.x + self.y * self.y)
    fn op +(self, other: Vec2) -> Vec2 = Vec2 { x: self.x + other.x, y: self.y + other.y }
    static fn zero() -> Vec2 = Vec2 { x: 0.0, y: 0.0 }
}

// Extend an enum with methods
extend Direction {
    fn opposite(self) -> Direction {
        match self {
            case .North { return .South }
            case .South { return .North }
            case .East  { return .West }
            case .West  { return .East }
        }
    }
}

// Interface conformance
extend Vec2 impl Drawable {
    fn draw(self) const -> string = f"({self.x}, {self.y})"
}

// Generic extend with constraints
extend <T impl Comparable> Pair<T> {
    fn max(self) const -> T = if self.first > self.second { self.first } else { self.second }
}