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:
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:
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:
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:
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:
// 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:
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:
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:
- Methods defined on the type itself
- Extension methods in the current module
- 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:
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:
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():
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:
// 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 methodsImport what you need:
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
// 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) -> boolKeep Methods Focused
// 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
// 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:
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
Integer utilities: Add extension methods to
intforis_positive(),is_negative(), andordinal()(1 → "1st", 2 → "2nd", …).String validation: Write plain functions
is_email(s: string),is_url(s: string), andis_phone_number(s: string)using the built-in string methods.Temperature conversions: Add extension methods to
floatforto_fahrenheit()andto_celsius().Enum helpers: Define a
Statusenum and add extension methods likeis_active()andlabel().Fluent API: Create a
QueryBuildertype and add extension methods that enable method chaining for building SQL queries.Domain methods: For a
Usertype, add extension methods for domain-specific operations likecan_edit(),can_delete(),is_admin().Organize: Create an
extensionsmodule with submodules for different types. Import and use them in main.
Next: Chapter 12: Concurrency