Classes & OOP
Object-oriented programming in Soli: classes, inheritance, interfaces, and static members.
Tip: Use < as an alias for extends (e.g., class Dog < Animal).
Basic Class Definition
class
Defines a class with properties and methods.
class Person
name: String;
age: Int;
email: String;
new(name: String, age: Int, email: String = null)
this.name = name;
this.age = age;
this.email = email ?? "";
end
def greet -> String
"Hello, I'm " + this.name
end
def introduce -> String
let intro = "Hi, I'm " + this.name + " and I'm " + str(this.age) + " years old";
if (this.email != "")
intro = intro + ". You can reach me at " + this.email;
end
intro
end
def have_birthday
this.age = this.age + 1;
end
end
Visibility Modifiers
| Modifier | Access |
|---|---|
| public | Accessible from anywhere (default) |
| private | Intended for use only within the class |
| protected | Intended for use within class and subclasses |
Note: Visibility modifiers are currently parsed for documentation purposes but not enforced at runtime.
Private Method Convention
In addition to the private keyword, Soli follows a naming convention where methods starting with an underscore (_) are considered internal/private helpers:
class PostsController < Controller
# Underscore prefix marks internal helper methods
def _permit_params(params: Any) {
"title": params["title"],
"content": params["content"]
}
def create(req: Any) # Using the private helper
let permitted = this._permit_params(req["params"]);
Posts.create(permitted)
end
end
Recommendation: Use either private def keyword OR underscore prefix (_) for internal helper methods. The underscore convention is used throughout the codebase (e.g., _authenticate, _build_post_params in examples).
class BankAccount
public account_number: String;
private balance: Float;
new(account_number: String, initial_deposit: Float)
this.account_number = account_number;
this.balance = initial_deposit;
public def deposit(amount: Float) -> Bool
if (this.validate_amount(amount))
this.balance = this.balance + amount;
return true;
end
return false;
end
let account = new BankAccount("123456789", 1000.0);
account.deposit(500.0); # Works - public method
print(account.get_balance()); # 1500.0
Static Members
static
Properties and methods that belong to the class, not instances.
class MathUtils
static PI: Float = 3.14159265359;
static E: Float = 2.71828182846;
static def square(x: Float) -> Float
x * x
static def cube(x: Float) -> Float
x * x * x
static def max(a: Float, b: Float) -> Float
if (a > b) a else b
end
static def clamp(value: Float, min_val: Float, max_val: Float) -> Float
if (value < min_val)
return min_val;
end
if (value > max_val)
return max_val;
end
value
end
# Using static members
print(MathUtils.PI); # 3.14159265359
print(MathUtils.square(4.0)); # 16.0
print(MathUtils.cube(3.0)); # 27.0
print(MathUtils.clamp(150, 0, 100)); # 100
this and super
Reference to the current instance.
class User
def new(name)
this.name = name;
def say_hello
println("Hello, " + this.name);
end
Inheritance.
class Admin < User
def say_hello
super.say_hello();
println("I am an admin.");
end
Static methods.
class Math
static def add(a, b)
a + b
end
Nested Classes
Classes can be defined inside other classes, providing logical grouping and access to the outer class's members.
class Outer
outer_value: Int;
new(value: Int)
this.outer_value = value;
def create_inner(x: Int) -> Inner
new Inner(this, x)
class Inner
outer: Outer;
inner_value: Int;
new(outer: Outer, value: Int)
this.outer = outer;
this.inner_value = value;
def get_combined -> Int
this.outer.outer_value + this.inner_value
def get_outer_value -> Int
this.outer.outer_value
end
# Using nested classes
let outer = new Outer(10);
let inner = new Outer.Inner(outer, 5);
print(inner.get_combined()); # 15
print(inner.get_outer_value()); # 10
# Factory pattern
let inner2 = outer.create_inner(20);
print(inner2.get_combined()); # 30
Accessing Nested Classes
Nested classes can be accessed through the outer class using dot notation.
class Tree
class Node
value: Int;
left: Node?;
right: Node?;
new(value: Int)
this.value = value;
this.left = null;
this.right = null;
def insert(new_value: Int)
if (new_value < this.value) {
if (this.left == null) {
this.left = new Tree.Node(new_value);
} else {
this.left.insert(new_value);
}
} else {
if (this.right == null) {
this.right = new Tree.Node(new_value);
} else {
this.right.insert(new_value);
}
}
root: Tree.Node?;
new()
this.root = null;
def insert(value: Int)
if (this.root == null) {
this.root = new Tree.Node(value);
} else {
this.root.insert(value);
}
end
let tree = new Tree();
tree.insert(5);
tree.insert(3);
tree.insert(7);
Domain-Driven Naming Convention
Nested classes with the :: separator follow a domain-driven naming convention, commonly used to organize related classes into logical namespaces. This pattern groups related functionality under a domain or context.
# Domain model organization
class User
class Profile
username: String;
avatar_url: String;
new(username: String)
this.username = username;
this.avatar_url = "https://example.com/avatars/" + username;
class Settings
theme: String;
notifications: Bool;
new()
this.theme = "light";
this.notifications = true;
end
let profile = new User::Profile("alice");
let settings = new User::Settings();
# Controller action organization
class Posts
class Action
def create(title: String, content: String)
"Creating post: " + title
def delete(id: Int)
"Deleting post: " + str(id)
class Validator
def validate_post(post: Any) -> Bool
post["title"] != null && post["content"] != null
end
let action = new Posts::Action();
action.create("Hello", "World");
Fully Qualified Names
Nested classes can be accessed using fully qualified names from anywhere in the code.
class Service
class Database
def query(sql: String)
"Executing: " + sql
class Cache
def get(key: String) -> String?
return null;
end
# Accessing from any scope using fully qualified names
let db = new Service::Database();
let cache = new Service::Cache();
print(db.query("SELECT * FROM users"));
print(cache.get("session:123"));