Skip to content

Chapter 11: Extension Methods ​

Adding Methods to Existing Types ​

You've seen methods on structs. But what if you want to add methods to types you don't own? Built-in types like int and float? Types from other modules?

Extension methods let you do this.

The Syntax ​

Define a method outside the type definition:

wyn
fn int.is_even(self) -> bool {
    return self % 2 == 0
}

fn main() -> int {
    x = 42
    print(x.is_even())  // true
    
    y = 17
    print(y.is_even())  // false
    
    return 0
}

The syntax is fn TypeName.method_name(self) -> ReturnType. The self parameter is the value you're calling the method on.

Now every integer has an is_even() method. You didn't modify the int type-you extended it.

Extending Built-in Types ​

Add methods to floats:

wyn
fn float.to_fahrenheit(self) -> float {
    return self * 9.0 / 5.0 + 32.0
}

fn main() -> int {
    celsius = 100.0
    print(celsius.to_fahrenheit())  // 212.0
    
    return 0
}

And to bools:

wyn
fn bool.yes_no(self) -> string {
    if (self) {
        return "yes"
    }
    return "no"
}

fn main() -> int {
    ready = true
    print(ready.yes_no())  // "yes"
    
    return 0
}

You can extend int, float, bool, and your own structs and enums.

Strings and arrays are the exception: they can't take extension methods yet. In exchange, they ship with a rich set of built-in methods:

wyn
fn main() -> int {
    numbers = [1, 2, 3, 4, 5]
    print(numbers.sum())      // 15
    print(numbers.average())  // 3.0
    
    text = "123"
    print(text.is_numeric())  // true
    
    return 0
}

Extending Custom Types ​

You can extend types from other modules:

models/user.wyn:

wyn
pub struct User {
    name: string,
    age: int
}

extensions/user_ext.wyn:

wyn
import models.user

fn User.is_adult(self) -> bool {
    return self.age >= 18
}

fn User.display_name(self) -> string {
    return "${self.name} (${self.age})"
}

main.wyn:

wyn
import models.user
import extensions.user_ext

fn main() -> int {
    // Not `user = ...`: `user` is the imported module's name here.
    alice = User { name: "Alice", age: 30 }
    print(alice.is_adult())       // true
    print(alice.display_name())   // "Alice (30)"
    return 0
}

This is powerful. You can add domain-specific methods to types without modifying their source code.

One caveat on today's compiler: the three-file split above type-checks, but an extension method defined in one module and called from another does not yet link. Until it does, keep fn User.is_adult in the same file as the code that calls it.

Method Chaining ​

Methods that return a value can be chained-extension methods included:

wyn
fn int.double(self) -> int {
    return self * 2
}

fn int.plus_one(self) -> int {
    return self + 1
}

fn main() -> int {
    x = 5
    print(x.double().plus_one().double())  // 22
    
    // Built-in string methods chain the same way
    text = "  hello  "
    print(text.trim().upper().reverse())   // "OLLEH"
    return 0
}

Each method returns a value, so you can chain them. This reads naturally: double, then add one, then double again.

Static Methods ​

Methods don't have to take self:

wyn
struct User {
    name: string,
    age: int
}

fn User.default() -> User {
    return User {
        name: "Guest",
        age: 0
    }
}

fn main() -> int {
    user = User::default()
    print(user.name)  // "Guest"
    return 0
}

Call static methods with :: instead of .. They're like constructors or factory functions.

Method Resolution ​

When you call a method, the compiler looks for it in this order:

  1. Methods defined on the type itself
  2. Extension methods in the current module
  3. Extension methods from imported modules

If multiple extension methods have the same name, the compiler uses the most specific one. If there's ambiguity, you get a compile error.

This prevents accidental conflicts. You can't accidentally override a method from the type's original definition.

Real-World: String Utilities ​

Strings don't take extension methods yet, so string utilities are plain functions-many of the common ones are already built in:

wyn
fn truncate(s: string, max_len: int) -> string {
    if (s.len() <= max_len) {
        return s
    }
    return s.substring(0, max_len) + "..."
}

fn main() -> int {
    // Built-in string methods
    print("123".is_numeric())        // true
    print("12a".is_numeric())        // false
    print("ha".repeat(3))            // "hahaha"
    
    // Plain function for the rest
    long = "This is a long string"
    print(truncate(long, 10))        // "This is a ..."
    
    return 0
}

Real-World: Result Helpers ​

Result ships with these helpers built in-unwrap_or, is_ok, and is_err:

wyn
fn success() -> Result<int, string> {
    return Ok(42)
}

fn failure() -> Result<int, string> {
    return Err("failed")
}

fn main() -> int {
    result1 = success()
    result2 = failure()
    
    print(result1.unwrap_or(0))  // 42
    print(result2.unwrap_or(0))  // 0
    
    print(result1.is_ok())       // true
    print(result2.is_err())      // true
    
    return 0
}

Real-World: Array Utilities ​

Arrays also come with a full utility belt built in-first(), last(), is_empty(), min(), max(), sum():

wyn
fn main() -> int {
    numbers = [3, 7, 2, 9, 1]
    
    print(numbers.first())     // 3
    print(numbers.last())      // 1
    print(numbers.max())       // 9
    print(numbers.min())       // 1
    print(numbers.sum())       // 22
    print(numbers.is_empty())  // false
    
    return 0
}

When to Use Extension Methods ​

Use extension methods when:

  • You want to add domain-specific methods to existing types
  • You want to create fluent APIs
  • You want to organize related functionality
  • You can't modify the original type

Don't use extension methods when:

  • The method should be part of the type's core API
  • The method needs access to private fields
  • The method is too general (might conflict with others)

Examples:

wyn
// Good: domain-specific
fn User.can_access(self, resource: string) -> bool

// Good: utility
fn int.is_even(self) -> bool

// Bad: too general (might conflict)
fn int.process(self) -> int

// Bad: duplicating the core API
fn int.absolute(self) -> int  // abs() already exists!

Organizing Extension Methods ​

Group related extensions in modules:

extensions/
├── int_ext.wyn       # Integer utilities
├── order_ext.wyn     # Order domain methods
└── user_ext.wyn      # User domain methods

Import what you need:

wyn
import models.user
import extensions.user_ext

fn main() -> int {
    alice = User { name: "Alice", age: 30 }
    print(alice.is_adult())       // From user_ext
    print(alice.display_name())   // From user_ext

    return 0
}

Best Practices ​

Name Methods Clearly ​

wyn
// Good: clear purpose
fn User.is_valid_email(self) -> bool
fn User.has_permission(self, perm: string) -> bool

// Bad: vague
fn User.check(self) -> bool
fn User.test(self, x: string) -> bool

Keep Methods Focused ​

wyn
// Good: single responsibility
fn Order.subtotal(self) -> float
fn Order.tax(self) -> float
fn Order.total(self) -> float

// Bad: doing too much
fn Order.process(self, mode: int) -> float  // What does mode mean?

Document Extension Methods ​

wyn
// Checks if the user's email address looks valid.
// Returns true if it contains @ and ., false otherwise.
fn User.is_valid_email(self) -> bool {
    return self.email.contains("@") and self.email.contains(".")
}

What You've Learned ​

Extension methods let you add methods to existing types. Use the syntax fn TypeName.method_name(self).

You can extend int, float, bool, enums, types from other modules, and your own structs. Strings and arrays rely on their built-in method sets instead. This enables fluent APIs and domain-specific methods.

Method chaining works because methods return values. Static methods don't take self and are called with ::.

The compiler resolves methods in a specific order, preventing conflicts. Extension methods can't override original methods.

Organize extensions in separate modules. Import what you need. Keep methods focused and well-named.

Traits ​

Traits define shared behavior across types. Implement one with impl Trait for Struct rather than with a bare fn Type.method extension - an extension method's name is global, so fn Point.describe and fn Color.describe collide, while two impl blocks of the same trait do not:

wyn
trait Printable {
    fn describe(self) -> string
}

struct Point { x: int, y: int }
struct Color { r: int, g: int, b: int }

impl Printable for Point {
    fn describe(self) -> string {
        return "Point(${self.x}, ${self.y})"
    }
}

impl Printable for Color {
    fn describe(self) -> string {
        return "Color(${self.r},${self.g},${self.b})"
    }
}

fn render(s: Printable) {
    print(s.describe())
}

fn main() -> int {
    var p = Point { x: 3, y: 4 }
    var c = Color { r: 255, g: 0, b: 0 }
    render(p)   // Point(3, 4)
    render(c)   // Color(255,0,0)
    return 0
}

Any type that implements the trait's methods can be passed where the trait is expected. This is Wyn's approach to polymorphism - no inheritance, just shared interfaces.

Bind the value to a variable before handing it to a trait-typed parameter, as above. Passing a struct literal straight in (render(Point { x: 3, y: 4 })) type-checks but does not compile yet.

See Traits for the full picture.

Exercises ​

  1. Integer utilities: Add extension methods to int for is_positive(), is_negative(), and ordinal() (1 → "1st", 2 → "2nd", …).

  2. String validation: Write plain functions is_email(s: string), is_url(s: string), and is_phone_number(s: string) using the built-in string methods.

  3. Temperature conversions: Add extension methods to float for to_fahrenheit() and to_celsius().

  4. Enum helpers: Define a Status enum and add extension methods like is_active() and label().

  5. Fluent API: Create a QueryBuilder type and add extension methods that enable method chaining for building SQL queries.

  6. Domain methods: For a User type, add extension methods for domain-specific operations like can_edit(), can_delete(), is_admin().

  7. Organize: Create an extensions module with submodules for different types. Import and use them in main.


Next: Chapter 12: Concurrency

MIT License - v1.22.0