Interfaces and Traits
Tova provides two mechanisms for defining shared behavior across types: interfaces and traits. Both let you specify a set of methods that a type must implement, enabling polymorphism and code reuse.
Interfaces
An interface declares a contract -- a set of method signatures that any implementing type must provide:
interface Printable {
fn to_string() -> String
}Implementing an Interface
Use impl to provide the method bodies for a specific type:
type User {
id: Int
name: String
email: String
}
impl Printable for User {
fn to_string() {
"{self.name} <{self.email}>"
}
}Now any User value can call .to_string():
alice = User(1, "Alice", "alice@example.com")
print(alice.to_string()) // "Alice <alice@example.com>"Multiple Interfaces
A type can implement as many interfaces as needed:
interface Printable {
fn to_string() -> String
}
interface Comparable {
fn compare(other) -> Int
}
type Temperature {
degrees: Float
unit: String
}
impl Printable for Temperature {
fn to_string() {
"{self.degrees}{self.unit}"
}
}
impl Comparable for Temperature {
fn compare(other) {
if self.degrees < other.degrees { -1 }
elif self.degrees > other.degrees { 1 }
else { 0 }
}
}Interfaces with Multiple Methods
Interfaces can require more than one method:
interface Collection {
fn length() -> Int
fn is_empty() -> Bool
fn contains(item) -> Bool
}
type Stack {
items: [Int]
}
impl Collection for Stack {
fn length() {
len(self.items)
}
fn is_empty() {
len(self.items) == 0
}
fn contains(item) {
item in self.items
}
}Traits
Traits work similarly to interfaces. Use trait to declare shared behavior:
trait Serializable {
fn serialize() -> String
}
trait Deserializable {
fn deserialize(data: String) -> Self
}Implementing Traits
The impl syntax is the same:
type Config {
host: String
port: Int
debug: Bool
}
impl Serializable for Config {
fn serialize() {
JSON.stringify({
host: self.host,
port: self.port,
debug: self.debug
})
}
}Traits with Default Implementations
Traits can provide default method bodies that implementing types inherit for free. Types can override them if needed:
trait Describable {
fn name() -> String
fn description() -> String {
"A {self.name()}"
}
}
type Car {
make: String
model: String
}
impl Describable for Car {
fn name() {
"{self.make} {self.model}"
}
// description() is inherited: "A Toyota Camry"
}When to Use Interface vs Trait
Both interface and trait define shared behavior. The choice between them is largely stylistic, but here is a general guideline:
- Use interface when you are defining a pure contract -- just method signatures, no default implementations.
- Use trait when you want to provide default method bodies that types can inherit or override.
Derive Macros
For common traits, Tova can automatically generate implementations with derive:
type Point {
x: Float
y: Float
} derive [Eq, Show, JSON]This generates:
- Eq -- equality (
==) and inequality (!=) based on field-by-field comparison. - Show -- a string representation for display/debugging.
- JSON --
to_json()andfrom_json()for serialization.
Derive with ADTs
Derive works with algebraic data types too:
type Shape {
Circle(radius: Float)
Rectangle(width: Float, height: Float)
} derive [Eq, Show]
a = Circle(5.0)
b = Circle(5.0)
c = Rectangle(3.0, 4.0)
print(a == b) // true
print(a == c) // false
print(a) // Circle(5.0)Available Derive Macros
| Macro | Generated Behavior |
|---|---|
Eq | == and != operators |
Show | Human-readable string representation |
JSON | .to_json() and .from_json() methods |
Putting It All Together
Here is a more complete example combining types, interfaces, traits, and derive:
interface Renderable {
fn render() -> String
}
type Heading {
level: Int
text: String
} derive [Eq, Show]
type Paragraph {
text: String
} derive [Eq, Show]
type Bold {
text: String
} derive [Eq, Show]
impl Renderable for Heading {
fn render() {
tag = "h{self.level}"
"<{tag}>{self.text}</{tag}>"
}
}
impl Renderable for Paragraph {
fn render() {
"<p>{self.text}</p>"
}
}
impl Renderable for Bold {
fn render() {
"<strong>{self.text}</strong>"
}
}
fn render_all(elements) {
elements.map(fn(el) el.render()).join("\n")
}
doc = [
Heading(1, "Welcome"),
Paragraph("This is a paragraph."),
Heading(2, "Details"),
Paragraph("More text with a "),
Bold("bold word"),
Paragraph(".")
]
print(render_all(doc))Plain impl Blocks
You can use impl without a trait to add methods and associated functions directly to a type:
Instance Methods
Functions with a self parameter are instance methods — called on an instance via dot notation:
type Point {
x: Float
y: Float
}
impl Point {
fn distance(self, other) {
dx = other.x - self.x
dy = other.y - self.y
Math.sqrt(dx * dx + dy * dy)
}
fn magnitude(self) {
Math.sqrt(self.x * self.x + self.y * self.y)
}
fn scale(self, factor) {
Point(self.x * factor, self.y * factor)
}
}The self parameter refers to the instance the method is called on:
a = Point(0, 0)
b = Point(3, 4)
print(a.distance(b)) // 5
print(b.magnitude()) // 5
print(b.scale(2)) // Point(6, 8)Associated Functions
Functions without self are associated functions — called on the type itself, not on an instance. Use these for constructors, factory methods, and type-level utilities:
impl Point {
fn origin() {
Point(0.0, 0.0)
}
fn from_polar(r: Float, theta: Float) {
Point(r * Math.cos(theta), r * Math.sin(theta))
}
fn unit_x() {
Point(1.0, 0.0)
}
}Call them on the type name:
o = Point.origin() // Point(0.0, 0.0)
p = Point.from_polar(1.0, 0.0) // Point(1.0, 0.0)
i = Point.unit_x() // Point(1.0, 0.0)TIP
The distinction is simple: if the function needs access to an existing instance, add self as the first parameter. If it creates a new value or doesn't need an instance, omit self.
Mixing Both
A single impl block can contain both instance methods and associated functions:
type Rect {
width: Float
height: Float
}
impl Rect {
// Associated functions (no self) — called as Rect.square(5.0)
fn square(size: Float) {
Rect(size, size)
}
// Instance methods (self) — called as rect.area()
fn area(self) {
self.width * self.height
}
fn is_square(self) {
self.width == self.height
}
}
r = Rect.square(5.0)
print(r.area()) // 25.0
print(r.is_square()) // trueInstance methods are compiled to prototype methods (shared across all instances). Associated functions are placed directly on the constructor function.
You can combine plain impl blocks with trait implementations on the same type:
impl Point {
fn translate(self, dx, dy) {
Point(self.x + dx, self.y + dy)
}
}
impl Printable for Point {
fn to_string() {
"({self.x}, {self.y})"
}
}Practical Tips
Derive early and often. Adding derive [Eq, Show] to a type costs nothing and gives you equality checks and debug printing for free. Add JSON when the type needs to cross a serialization boundary.
Keep interfaces small. An interface with one or two methods is easier to implement and compose than one with ten. Prefer multiple small interfaces over one large one.
Use impl blocks to organize related behavior. Even for a single type, splitting behavior into separate impl blocks by interface keeps your code modular:
type User {
id: Int
name: String
email: String
} derive [Eq, JSON]
impl Printable for User {
fn to_string() { self.name }
}
impl Comparable for User {
fn compare(other) { self.id - other.id }
}