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"]
}
end
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
end
public def deposit(amount: Float) -> Bool
if (this.validate_amount(amount))
this.balance = this.balance + amount
return true
end
return false
end
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
end
static def cube(x: Float) -> Float
x * x * x
end
static def max(a: Float, b: Float) -> Float
if (a > b) then return a end
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
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
new(name)
this.name = name
end
def say_hello
println("Hello, " + this.name)
end
end
Inheritance with super.
class Admin < User
def say_hello
super.say_hello()
println("I am an admin.")
end
end
Static methods.
class Math
static def add(a, b)
a + b
end
end
Multi-level Inheritance
Classes can extend other user-defined classes, forming deep inheritance chains. Methods, fields, constructors, and static methods are all inherited through the full chain.
class Animal
name: String
new(name: String)
this.name = name
end
def speak -> String
"..."
end
end
class Pet extends Animal
owner: String
new(name: String, owner: String)
super(name)
this.owner = owner
end
def describe -> String
this.name + " belongs to " + this.owner
end
end
class Dog extends Pet
def speak -> String
"Woof!"
end
end
let dog = new Dog("Rex", "Alice")
print(dog.speak()) # "Woof!"
print(dog.describe()) # "Rex belongs to Alice"
print(dog.name) # "Rex" (inherited from Animal)
Super Chaining
Each class's super refers to its direct parent, allowing calls to chain up through the hierarchy.
class A
def identify -> String
"A"
end
end
class B extends A
def identify -> String
super.identify() + " -> B"
end
end
class C extends B
def identify -> String
super.identify() + " -> C"
end
end
print(new C().identify()) # "A -> B -> C"
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
end
def create_inner(x: Int) -> Inner
new Inner(this, x)
end
class Inner
outer: Outer
inner_value: Int
new(outer: Outer, value: Int)
this.outer = outer
this.inner_value = value
end
def get_combined -> Int
this.outer.outer_value + this.inner_value
end
def get_outer_value -> Int
this.outer.outer_value
end
end
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
end
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)
end
else
if (this.right == null)
this.right = new Tree::Node(new_value)
else
this.right.insert(new_value)
end
end
end
end
root: Tree::Node?
new
this.root = null
end
def insert(value: Int)
if (this.root == null)
this.root = new Tree::Node(value)
else
this.root.insert(value)
end
end
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
end
end
class Settings
theme: String
notifications: Bool
new
this.theme = "light"
this.notifications = true
end
end
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
end
def delete(id: Int)
"Deleting post: " + str(id)
end
end
class Validator
def validate_post(post: Any) -> Bool
post["title"] != null && post["content"] != null
end
end
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
end
end
class Cache
def get(key: String) -> String?
return null
end
end
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"))