Traits

Define shared behavior and provide statically dispatched implementations for different structs.

1

Core concepts

trait, impl, self, method calls, static dispatch

2

How the learner should approach this project

Do not paste the complete program immediately. Create the file, type the first step, run vpp check, and then run the program when a complete entry point exists. Read the explanation before looking at the snippet. After each step, predict what the new code should do. This turns the page into a lesson rather than a code dump.

3

Step 1: Define Circle

Circle is a concrete type that will later implement Area. Keeping the data definition separate from the behavior makes the role of the trait easier to see.

After typing the snippet, identify the new names introduced by the step. Ask what each name represents, what type the compiler should infer or check, and what value should exist after the code runs.

main.vpp
struct Circle {
    radius: int
}
4

Step 2: Define Square

Square is another concrete type that can provide its own implementation of the same Area behavior.

Before moving forward, explain how this step connects to the previous one. In particular, identify which values cross a function boundary, which values change, and which values are guaranteed by the type system.

main.vpp
struct Square {
    side: int
}
5

Step 3: Declare the Area trait

The trait declares a required method. It describes behavior rather than storage. Implementations are selected statically by the compiler.

Before moving forward, explain how this step connects to the previous one. In particular, identify which values cross a function boundary, which values change, and which values are guaranteed by the type system.

main.vpp
trait Area {
    fn area(self) -> int
}
6

Step 4: Begin the Circle implementation

The impl block connects Area to Circle. The method signature must satisfy the trait requirement.

Before moving forward, explain how this step connects to the previous one. In particular, identify which values cross a function boundary, which values change, and which values are guaranteed by the type system.

main.vpp
impl Area for Circle {
    fn area(self) -> int {
        return self.radius * self.radius * 3
    }
}
7

Step 5: Implement Area for Square

Square uses the same method name but a different calculation. Static dispatch means the compiler knows which implementation belongs to the concrete type.

Before moving forward, explain how this step connects to the previous one. In particular, identify which values cross a function boundary, which values change, and which values are guaranteed by the type system.

main.vpp
impl Area for Square {
    fn area(self) -> int {
        return self.side * self.side
    }
}
8

Step 6: Call methods from main

Method syntax makes the trait behavior easy to use. c.area selects the Circle implementation and s.area selects the Square implementation.

Before moving forward, explain how this step connects to the previous one. In particular, identify which values cross a function boundary, which values change, and which values are guaranteed by the type system.

main.vpp
fn main() -> int {
    let c = Circle { radius: 5 }
    let s = Square { side: 4 }
    print(c.area())
    print(s.area())
    return 0
}
9

Complete source

main.vpp
struct Circle {
    radius: int
}
struct Square {
    side: int
}
trait Area {
    fn area(self) -> int
}
impl Area for Circle {
    fn area(self) -> int {
        return self.radius * self.radius * 3
    }
}
impl Area for Square {
    fn area(self) -> int {
        return self.side * self.side
    }
}
fn main() -> int {
    let c = Circle { radius: 5 }
    let s = Square { side: 4 }
    print(c.area())
    print(s.area())
    return 0
}
10

Expected behavior

The complete program should produce the following output when run with the command shown below. Exact formatting should follow the current V++ runtime.

75
16
11

Run the project

terminal
vpp run projects/13-traits/main.vpp
12

What the learner should understand after this project

Traits describe shared behavior while impl blocks provide concrete behavior. Static dispatch lets the compiler resolve the method for a known concrete type.

13

Common mistakes to teach

  1. Trait method signature not matching the trait
  2. Implementing the trait for the wrong type
  3. Calling a method that the type does not implement
14

Practice extension

Create a Rectangle struct and implement Area for it. Explain why the method name can remain the same.

A strong learner should be able to explain the program without looking at the code, rebuild the core idea from memory, and make the practice change without copying a solution.

15

Full program

main.vpp
struct Circle {
    radius: int
}
struct Square {
    side: int
}
trait Area {
    fn area(self) -> int
}
impl Area for Circle {
    fn area(self) -> int {
        return self.radius * self.radius * 3
    }
}
impl Area for Square {
    fn area(self) -> int {
        return self.side * self.side
    }
}
fn main() -> int {
    let c = Circle { radius: 5 }
    let s = Square { side: 4 }
    print(c.area())
    print(s.area())
    return 0
}
vpp
$ ready. Click Test program.