Extensions

Extensions let you add methods to existing traits without modifying them. This enables extending library code and creating domain-specific utilities.

What Are Extensions?

Extensions add new methods to traits after they’re defined:

// Original trait in std library
trait Iterator {
    type Item;
    @next (self) -> (Option<Self.Item>, Self);
}

// Your extension adds new methods
extend Iterator {
    @sum (self) -> int where Self.Item == int =
        self.fold(initial: 0, op: (acc, x) -> acc + x);
}

Now any Iterator of int has a .sum() method.

Defining Extensions

Basic Extension

extend Iterator {
    @count_where (self, predicate: (Self.Item) -> bool) -> int =
        self.filter(predicate: predicate).count();
}

Extension with Constraints

Use where to limit which types can use the method:

extend Iterator {
    @sum (self) -> int where Self.Item == int =
        self.fold(initial: 0, op: (acc, x) -> acc + x);

    @product (self) -> int where Self.Item == int =
        self.fold(initial: 1, op: (acc, x) -> acc * x);

    @join_strings (self, sep: str) -> str where Self.Item == str =
        self.fold(initial: "", op: (acc, s) -> if acc == "" then s else `{acc}{sep}{s}`);
}

Extension with Generic Constraints

extend Iterator {
    @max_by<K: Comparable> (self, key: (Self.Item) -> K) -> Option<Self.Item> = {
        let result: Option<(Self.Item, K)> = None;

        for item in self do {
            let k = key(item);
            result = match result {
                None -> Some((item, k))
                Some((_, max_k)) -> if k > max_k then Some((item, k)) else result
            }
        };

        result.map(transform: (item, _) -> item)
    }
}

Importing Extensions

Extensions must be explicitly imported to use:

// Import specific extension methods
extension std.iter.extensions { Iterator.sum, Iterator.product };

// Import from local file
extension "./my_extensions" { Iterator.count_where };

// Now you can use them
let numbers = [1, 2, 3, 4, 5];
let total = numbers.iter().sum();  // 15

Import Syntax

extension "path" { Trait.method, Trait.other_method };
extension module.path { Trait.method };

Using Extensions

Once imported, use them like regular methods:

extension std.iter.extensions { Iterator.sum, Iterator.product };

let numbers = [1, 2, 3, 4, 5];
let total = numbers.iter().sum();      // 15
let product = numbers.iter().product(); // 120

Extension Patterns

Domain-Specific Utilities

// extensions/money.ori
type Money = { amount: int, currency: str }

extend Iterator {
    @total_amount (self) -> int where Self.Item == Money =
        self.map(transform: m -> m.amount).fold(initial: 0, op: (a, b) -> a + b);

    @by_currency (self) -> {str: [Money]} where Self.Item == Money = {
        let groups: {str: [Money]} = {};
        for money in self do {
            let current = groups[money.currency] ?? [];
            groups = { ...groups, money.currency: [...current, money] }
        };
        groups
    }
}
// Usage
extension "./extensions/money" { Iterator.total_amount, Iterator.by_currency };

let transactions = [
    Money { amount: 100, currency: "USD" },
    Money { amount: 50, currency: "EUR" },
    Money { amount: 75, currency: "USD" },
];

let usd_total = transactions.iter()
    .filter(predicate: m -> m.currency == "USD")
    .total_amount();  // 175

Statistics Extensions

// extensions/stats.ori
extend Iterator {
    @mean (self) -> float where Self.Item == float = {
        let sum = 0.0;
        let count = 0;
        for x in self do {
            sum = sum + x;
            count = count + 1
        };
        if count == 0 then 0.0 else sum / count as float
    }

    @variance (self) -> float where Self.Item == float = {
        let values = self.collect();
        let m = values.iter().mean();
        let squared_diffs = values.iter()
            .map(transform: x -> (x - m) * (x - m));
        squared_diffs.mean()
    }

    @std_dev (self) -> float where Self.Item == float =
        self.variance().sqrt();
}

String Extensions

// extensions/strings.ori
extend Iterator {
    @join (self, sep: str) -> str where Self.Item == str = {
        let result = "";
        let first = true;
        for s in self do {
            result = if first then s else `{result}{sep}{s}`;
            first = false
        };
        result
    }

    @lines (self) -> [str] where Self.Item == char = {
        let lines: [str] = [];
        let current = "";
        for c in self do
            if c == '\n' then {
                lines = [...lines, current];
                current = ""
            } else
                current = `{current}{c}`;
        if current != "" then
            lines = [...lines, current];
        lines
    }
}

Numeric Extensions

// extensions/numeric.ori
extend Iterator {
    @running_sum (self) -> impl Iterator where Self.Item == int = {
        let total = 0;
        self.map(transform: x -> {
            total = total + x;
            total
        })
    }

    @differences (self) -> impl Iterator where Self.Item == int = {
        let prev: Option<int> = None;
        self.filter_map(transform: x -> {
            let result = prev.map(transform: p -> x - p);
            prev = Some(x);
            result
        })
    }
}

Extension vs Default Implementation

FeatureDefault in TraitExtension
DefinedInside trait definitionOutside trait
VisibilityAlways availableRequires import
OverrideImplementers can overrideCannot be overridden
Use caseCore functionalityOptional utilities

When to Use Extensions

  • Adding convenience methods without bloating the trait
  • Domain-specific utilities
  • Experimental features
  • Methods that only make sense for certain type combinations

When to Use Default Implementations

  • Methods that all implementers should have
  • Core functionality of the trait
  • Methods that implementers might want to optimize

Testing Extensions

// extensions/iter_extensions.ori
extend Iterator {
    @sum (self) -> int where Self.Item == int =
        self.fold(initial: 0, op: (acc, x) -> acc + x);
}

// Test file
extension "./iter_extensions" { Iterator.sum };

@test_sum tests _ () -> void = {
    let numbers = [1, 2, 3, 4, 5];
    assert_eq(actual: numbers.iter().sum(), expected: 15);

    let empty: [int] = [];
    assert_eq(actual: empty.iter().sum(), expected: 0)
}

Complete Example

// extensions/collections.ori

// Grouping extension
extend Iterator {
    @group_by<K: Eq + Hashable> (
        self,
        key: (Self.Item) -> K,
    ) -> {K: [Self.Item]} = {
        let groups: {K: [Self.Item]} = {};
        for item in self do {
            let k = key(item);
            let current = groups[k] ?? [];
            groups = { ...groups, k: [...current, item] }
        };
        groups
    }
}

// Partitioning extension
extend Iterator {
    @partition (
        self,
        predicate: (Self.Item) -> bool,
    ) -> ([Self.Item], [Self.Item]) = {
        let matching: [Self.Item] = [];
        let not_matching: [Self.Item] = [];
        for item in self do
            if predicate(item) then
                matching = [...matching, item]
            else
                not_matching = [...not_matching, item];
        (matching, not_matching)
    }
}

// Chunking extension
extend Iterator {
    @chunks (self, size: int) -> [[Self.Item]] = {
        let result: [[Self.Item]] = [];
        let current: [Self.Item] = [];
        for item in self do {
            current = [...current, item];
            if len(collection: current) == size then {
                result = [...result, current];
                current = []
            }
        };
        if !is_empty(collection: current) then
            result = [...result, current];
        result
    }
}

// Intersperse extension
extend Iterator {
    @intersperse (self, sep: Self.Item) -> impl Iterator = {
        let first = true;
        self.flat_map(transform: item -> {
            if first then {
                first = false;
                [item].iter()
            } else
                [sep, item].iter()
        })
    }
}
// Usage
extension "./extensions/collections" {
    Iterator.group_by,
    Iterator.partition,
    Iterator.chunks,
    Iterator.intersperse,
};

type Person = { name: str, age: int, city: str }

@example () -> void = {
    let people = [
        Person { name: "Alice", age: 30, city: "NYC" }
        Person { name: "Bob", age: 25, city: "LA" }
        Person { name: "Charlie", age: 35, city: "NYC" }
        Person { name: "Diana", age: 28, city: "LA" }
    ];

    // Group by city
    let by_city = people.iter().group_by(key: p -> p.city);
    // { "NYC": [Alice, Charlie], "LA": [Bob, Diana] }

    // Partition by age
    let (over_30, under_30) = people.iter().partition(predicate: p -> p.age >= 30);
    // ([Alice, Charlie], [Bob, Diana])

    // Chunk into pairs
    let pairs = people.iter().chunks(size: 2);
    // [[Alice, Bob], [Charlie, Diana]]

    // Join names with separator
    let names = people.iter()
        .map(transform: p -> p.name)
        .intersperse(sep: " and ")
        .collect()
        .join(sep: "");
    // "Alice and Bob and Charlie and Diana"
}

@test_group_by tests _ () -> void = {
    extension "./extensions/collections" { Iterator.group_by };

    let numbers = [1, 2, 3, 4, 5, 6];
    let grouped = numbers.iter().group_by(key: n -> n % 2);
    assert_eq(actual: len(collection: grouped[0] ?? []), expected: 3);
    assert_eq(actual: len(collection: grouped[1] ?? []), expected: 3)
}

@test_partition tests _ () -> void = {
    extension "./extensions/collections" { Iterator.partition };

    let numbers = [1, 2, 3, 4, 5, 6];
    let (evens, odds) = numbers.iter().partition(predicate: n -> n % 2 == 0);
    assert_eq(actual: evens, expected: [2, 4, 6]);
    assert_eq(actual: odds, expected: [1, 3, 5])
}

@test_chunks tests _ () -> void = {
    extension "./extensions/collections" { Iterator.chunks };

    let numbers = [1, 2, 3, 4, 5];
    let chunked = numbers.iter().chunks(size: 2);
    assert_eq(actual: chunked, expected: [[1, 2], [3, 4], [5]])
}

Quick Reference

Define Extension

extend Trait {
    @method (self) -> Type = ...;
}

extend Trait {
    @method (self) -> Type where Self.Item == int = ...;
}

Import Extension

extension "path" { Trait.method };
extension module.path { Trait.method };

Use Extension

value.method();  // After importing

What’s Next

Now that you understand extensions: