Why Classes?
Before diving into implementation, let us understand why we want classes. After all, Secondlang works fine with just primitive types (int and bool). If you have not read Why Types Matter, that chapter explains the benefits of static typing that we build upon here.
The Problem: Related Data Scattered
In Secondlang, we can only work with individual int and bool values:
# A "point" represented as separate variables
x1 = 10
y1 = 20
x2 = 30
y2 = 40
# Function takes 4 separate parameters
def distance_squared(x1: int, y1: int, x2: int, y2: int) -> int {
dx = x2 - x1
dy = y2 - y1
return dx * dx + dy * dy
}
distance_squared(x1, y1, x2, y2)
This has problems:
- Easy to mix up - What if we accidentally pass
y1twice? - No grouping -
x1andy1are related, but the language does not know that - Verbose - Every function needs all the pieces passed separately
- No encapsulation - Anyone can mess with
x1directly
The Solution: Group Data with Classes
With classes, we bundle related data and behavior together:
class Point {
x: int
y: int
def __init__(self, x: int, y: int) {
self.x = x
self.y = y
}
def distance_squared(self, other: Point) -> int {
dx = other.x - self.x
dy = other.y - self.y
return dx * dx + dy * dy
}
}
p1 = new Point(10, 20)
p2 = new Point(30, 40)
p1.distance_squared(p2) # Clean and clear!
Now:
- Cannot mix up -
p1is one thing,p2is another - Data is grouped -
xandybelong to aPoint - Concise - Pass one
Pointinstead of twoints - Behavior attached -
distance_squaredbelongs toPoint
Object-Oriented Programming (OOP)
Classes are the foundation of Object-Oriented Programming. The key concepts:
| Concept | Description | Example |
|---|---|---|
| Class | A blueprint for creating objects | class Point { ... } |
| Object | An instance of a class | p = new Point(1, 2) |
| Field | Data stored in an object | self.x, self.y |
| Method | Function attached to a class | def distance(self) |
| Constructor | Initializes a new object | def __init__(self) |
| Destructor | Cleans up before deletion | def __del__(self) |
Classes as Custom Types
The key insight: classes define new types.
In Secondlang, we have two types: int and bool. In Thirdlang, we can create infinitely many types:
class Point { x: int y: int ... }
class Counter { count: int ... }
class Rectangle { width: int height: int ... }
Each class name becomes a valid type:
def move(p: Point, dx: int, dy: int) -> Point { ... }
# ^^^^^ Point is now a type!
This is called a nominal type system - types are identified by their names.
Memory Model: Stack vs Heap
Here is where things get interesting. In Secondlang, everything lives on the stack. With classes, objects live on the heap, and variables hold pointers to them:
This has implications:
- Creation -
new Point(...)allocates memory on the heap - Access -
p.xfollows the pointer to read the field - Deletion -
delete pfrees the heap memory - Sharing - Multiple variables can point to the same object
Why Not Garbage Collection?
Many languages (Java, Python, Go) use garbage collection - automatically freeing memory when objects are no longer used.
We use explicit memory management instead:
p = new Point(1, 2) # Allocate
# ... use p ...
delete p # Free (programmer's job)
Why? For learning purposes:
- Understand the machine - See how memory really works
- Appreciate GC - Understand what garbage collectors do for you
- Simpler to implement - No need for reference counting or tracing
- Like C++ - Many real languages work this way
The downside: forget to delete and you have a memory leak. Delete twice and you have undefined behavior. Real programs need to be careful!
Our OOP Design
We implement a subset of OOP. Here is what we include and exclude:
What We Include
| Feature | Example |
|---|---|
| Class definition | class Point { ... } |
| Fields | x: int |
| Methods | def get_x(self) -> int |
| Constructor | def __init__(self, x: int) |
| Destructor | def __del__(self) |
| Object creation | new Point(1, 2) |
| Object deletion | delete p |
| Field access | p.x or self.x |
| Method calls | p.distance(other) |
| Classes as types | other: Point |
What We Exclude
| Feature | Why Excluded |
|---|---|
| Inheritance | Adds vtables, dynamic dispatch complexity |
| Interfaces/Traits | Would need trait objects or generics |
| Visibility (public/private) | Everything is public for simplicity |
| Static methods | Would need different dispatch |
| Operator overloading | Requires special method resolution |
These exclusions keep the implementation manageable while teaching the core concepts.
The Self Parameter
In Thirdlang, methods always take self as their first parameter:
def get_x(self) -> int {
return self.x
}
This is explicit, like Python. When you call p.get_x(), the object p is passed as self.
Compare to other languages:
| Language | Self/This |
|---|---|
| Python | def method(self): (explicit) |
| Rust | fn method(&self) (explicit) |
| Java/C++ | this (implicit) |
| Thirdlang | def method(self) (explicit, like Python) |
The explicit self makes it clear: methods are just functions that receive the object as their first argument.
What We Will Build
By the end of this section, you will understand:
- How classes are parsed into AST nodes
- How the type checker handles class types
- How objects are allocated on the heap
- How methods are compiled to regular functions
- How destructors and
deletework
Let us start by looking at the syntax and grammar for classes.