Skip to content

Polymorphism

Polymorphism allows child classes to override parent methods, and have those overrides called even when invoked from parent code. This is fundamental to object-oriented programming and enables powerful design patterns.

How It Works

When a parent method calls self:someMethod(), SIMPLOO looks up the method starting from the actual instance's class. If the child overrides that method, the child's version is called:

class "Animal" {
    speak = function(self)
        return "..."
    end;

    introduce = function(self)
        return "I say: " .. self:speak()  -- Polymorphic call
    end;
}

class "Dog" extends "Animal" {
    speak = function(self)
        return "woof!"
    end;
}

class "Cat" extends "Animal" {
    speak = function(self)
        return "meow!"
    end;
}

local dog = Dog.new()
local cat = Cat.new()

print(dog:introduce())  -- I say: woof!
print(cat:introduce())  -- I say: meow!

The parent's introduce() method calls self:speak(), but since self is a Dog or Cat instance, the child's override is used.

Calling Parent Methods

Use self.ParentName:method() to explicitly call the parent's version:

class "Animal" {
    speak = function(self)
        return "generic sound"
    end;
}

class "Dog" extends "Animal" {
    speak = function(self)
        return "woof"
    end;

    -- Can still access parent's version
    parentSpeak = function(self)
        return self.Animal:speak()
    end;
}

local dog = Dog.new()
print(dog:speak())        -- woof
print(dog:parentSpeak())  -- generic sound

Chaining Parent Calls

Each level can call its parent, building up results:

class "A" {
    getValue = function(self)
        return "A"
    end;
}

class "B" extends "A" {
    getValue = function(self)
        return "B+" .. self.A:getValue()
    end;
}

class "C" extends "B" {
    getValue = function(self)
        return "C+" .. self.B:getValue()
    end;
}

local c = C.new()
print(c:getValue())  -- C+B+A

Deep Inheritance

Polymorphism works through any depth of inheritance:

class "Base" {
    getName = function(self)
        return "base"
    end;

    callGetName = function(self)
        return self:getName()
    end;
}

class "Middle" extends "Base" {
    getName = function(self)
        return "middle"
    end;
}

class "Leaf" extends "Middle" {
    getName = function(self)
        return "leaf"
    end;
}

local leaf = Leaf.new()
print(leaf:callGetName())  -- leaf (calls Leaf's getName)

local middle = Middle.new()
print(middle:callGetName())  -- middle (calls Middle's getName)

Multiple Inheritance

Polymorphism works across multiple parent classes. A child can override methods from any parent, and methods from one parent can call methods inherited from another:

class "Describable" {
    getDescription = function(self)
        return "unknown"
    end;

    describe = function(self)
        return "I am: " .. self:getDescription()
    end;
}

class "Identifiable" {
    id = 0;

    getId = function(self)
        return self.id
    end;
}

class "Entity" extends "Describable, Identifiable" {
    name = "";

    __construct = function(self, id, name)
        self.id = id
        self.name = name
    end;

    -- Override from Describable, uses method from Identifiable
    getDescription = function(self)
        return self.name .. " (id:" .. self:getId() .. ")"
    end;
}

local e = Entity.new(42, "Player")
print(e:describe())  -- I am: Player (id:42)

Polymorphism in Constructors

When a parent constructor calls a virtual method, the child's override is used. Child members are initialized to their declared default values before any constructor runs, so the override can safely access them:

class "Widget" {
    name = "";

    __construct = function(self)
        self.name = self:getName()  -- Polymorphic call
    end;

    getName = function(self)
        return "Widget"
    end;
}

class "Button" extends "Widget" {
    label = "Click me";

    getName = function(self)
        return "Button:" .. self.label
    end;
}

local btn = Button.new()
print(btn.name)  -- Button:Click me

Note

If the child has its own constructor that modifies members, those changes happen after the parent constructor runs. The parent constructor sees the declared default values, not values set by the child constructor.

Design Patterns

Polymorphism enables many classic design patterns. Here are a few examples:

Template Method Pattern

Define an algorithm skeleton in the parent, let children fill in the steps:

class "DataProcessor" {
    -- Template method - defines the algorithm
    process = function(self)
        local data = self:fetchData()
        local transformed = self:transform(data)
        return self:format(transformed)
    end;

    fetchData = function(self)
        return "raw"
    end;

    transform = function(self, data)
        return data
    end;

    format = function(self, data)
        return "[" .. data .. "]"
    end;
}

class "JsonProcessor" extends "DataProcessor" {
    transform = function(self, data)
        return '{"data":"' .. data .. '"}'
    end;
}

local json = JsonProcessor.new()
print(json:process())  -- [{"data":"raw"}]

Factory Method Pattern

Let subclasses decide which objects to create:

class "Document" {
    content = "";

    createPage = function(self)
        return "GenericPage"
    end;

    addContent = function(self, text)
        self.content = self.content .. self:createPage() .. ":" .. text .. "\n"
    end;
}

class "Report" extends "Document" {
    createPage = function(self)
        return "ReportPage"
    end;
}

local report = Report.new()
report:addContent("Q1 Sales")
print(report.content)  -- ReportPage:Q1 Sales

State Pattern

Change behavior by swapping state objects:

class "State" {
    handle = function(self, context)
        return "default"
    end;
}

class "IdleState" extends "State" {
    handle = function(self, context)
        return "idle"
    end;
}

class "RunningState" extends "State" {
    handle = function(self, context)
        return "running at " .. context.speed
    end;
}

class "Machine" {
    state = null;
    speed = 100;

    __construct = function(self)
        self.state = IdleState.new()
    end;

    setState = function(self, state)
        self.state = state
    end;

    process = function(self)
        return self.state:handle(self)
    end;
}

local m = Machine.new()
print(m:process())  -- idle

m:setState(RunningState.new())
print(m:process())  -- running at 100

Visitor Pattern

Double dispatch for operations on object structures:

class "Visitor" {
    visitCircle = function(self, circle) return 0 end;
    visitSquare = function(self, square) return 0 end;
}

class "AreaVisitor" extends "Visitor" {
    visitCircle = function(self, circle)
        return 3.14 * circle.radius * circle.radius
    end;

    visitSquare = function(self, square)
        return square.side * square.side
    end;
}

class "Shape" {
    accept = function(self, visitor)
        return 0
    end;
}

class "Circle" extends "Shape" {
    radius = 0;

    __construct = function(self, r)
        self.radius = r
    end;

    accept = function(self, visitor)
        return visitor:visitCircle(self)
    end;
}

class "Square" extends "Shape" {
    side = 0;

    __construct = function(self, s)
        self.side = s
    end;

    accept = function(self, visitor)
        return visitor:visitSquare(self)
    end;
}

local shapes = {Circle.new(2), Square.new(3)}
local areaVisitor = AreaVisitor.new()

for _, shape in ipairs(shapes) do
    print(shape:accept(areaVisitor))
end
-- 12.56
-- 9

Private Members and Polymorphism

Private members are correctly scoped - each class accesses its own privates, even during polymorphic calls:

class "Parent" {
    private {
        secret = "parent secret";
    };

    public {
        getSecret = function(self)
            return self.secret
        end;
    };
}

class "Child" extends "Parent" {
    private {
        secret = "child secret";
    };

    public {
        getChildSecret = function(self)
            return self.secret
        end;

        callParentGetSecret = function(self)
            return self.Parent:getSecret()
        end;
    };
}

local c = Child.new()
print(c:getSecret())            -- parent secret
print(c:getChildSecret())       -- child secret
print(c:callParentGetSecret())  -- parent secret

The parent's getSecret() accesses the parent's private secret, not the child's, even when called on a child instance.