ESC
Type to search...
S
Soli Docs

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"))