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 {
    var x = 42
    print(x.is_even())  // true
    
    var 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 {
    var 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 {
    var 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 {
    var numbers = [1, 2, 3, 4, 5]
    print(numbers.sum())      // 15
    print(numbers.average())  // 3.0
    
    var text = "123"
    print(text.is_numeric())  // true
    
    return 0
}

Extending Custom Types

You can extend types from other modules:

wyn
// models/user.wyn
pub struct User {
    name: string,
    age: int
}

// extensions/user_ext.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
import models.user
import extensions.user_ext

fn main() -> int {
    var user = User { name: "Alice", age: 30 }
    print(user.is_adult())       // true
    print(user.display_name())   // "Alice (30)"
    return 0
}

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

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 {
    var x = 5
    print(x.double().plus_one().double())  // 22
    
    // Built-in string methods chain the same way
    var 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 {
    var 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
    var 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 {
    var result1 = success()
    var 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 {
    var 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 {
    var user = User { name: "Alice", age: 30 }
    print(user.is_adult())       // From user_ext
    print(user.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:

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

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

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

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

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

fn main() -> int {
    render(Point { x: 3, y: 4 })    // Point(3, 4)
    render(Color { r: 255, g: 0, b: 0 })  // 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.

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.19.1