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"]
        }
    

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