Skip to content

Extends

kairo-docs-bot edited this page Aug 19, 2026 · 2 revisions

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(const self) -> 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

Type Supports extend
Structs Yes the only way to add methods
Enums Yes the only way to add methods
Classes Yes
Unions No
Interfaces No

What Extends Can Add

The rules differ by type:

Structs

Allowed Not allowed
Methods Constructors
Static functions Destructors (fn op delete)
Arithmetic / comparison operators Copy / 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

Allowed Not allowed
Methods Constructors
Static functions Destructors
Comparison / arithmetic operators Copy / move assignment
fn op as (type conversion)

Classes

Allowed Not allowed
Methods Constructors
Static functions Destructors
All operators Copy / 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(const self) -> string
}

extend Point impl Drawable {
    fn draw(const self) -> 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(const self) -> string { ... }
}

extend Point impl Serializable {
    fn serialize(const self) -> [byte] { ... }
    fn byte_size(const self) -> 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(const self) -> 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(const self) -> i32
    fn get(const self, index: i32) -> T
}

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

    fn get(const self, index: i32) -> 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(const self, other: Point) -> f64 {
        return (self - other).length()
    }

    priv fn validate(const self) -> 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(const self) -> 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(const self) -> 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(const self) -> string
}

// This is legal even though Point is defined in point.k,
// because Drawable is defined here:
extend Point impl Drawable {
    fn draw(const self) -> 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(const self) -> [byte] { ... }
    fn byte_size(const self) -> i32 { return 3 }
}

// Display
extend Color impl Drawable {
    fn draw(const self) -> 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(const self) -> string {
        return f"Server(:{self.port})"
    }
}

Summary

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

extend Vec2 {
    fn length(const self) -> 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(const self) -> string = f"({self.x}, {self.y})"
}

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

Start here: Primitives


1. Fundamentals

2. Functions & Control Flow

3. Types

4. Modules & Metaprogramming

5. Memory & Safety

6. Interop & Concurrency


Website · Docs · Repo

Clone this wiki locally