PatLang Object Composition Styles: Inheritance, Traits, and Plain Composition
For new readers
PatLang gives you several ways to share behaviour and state between objects — inheriting from a parent class, mixing in a trait, or just building an object out of other objects — and they compose freely rather than forcing one style. This page walks each one in isolation with a runnable example, then shows them combined, with the tradeoffs made explicit rather than left implicit. See the Paradigms Guide's OO section for the syntax reference this page assumes.
Every example on this page was run directly (--ir-run) and, for the more involved ones, cross-checked against the compiled-native (--patc) and self-hosted (patc1.exe) paths too — not written from memory of the syntax. Real output is shown under each one.
Plain composition: objects built from other objects
No class declaration at all — just the underlying object-store primitives (new/get/send) storing one object as a field of another:
let engine = new("Engine", "e1")
send(engine, "set", "horsepower", 300)
let car = new("Car", "c1")
send(car, "set", "engine", engine)
print(get(get(car, "engine"), "horsepower"))
Output:
300
Pros: zero ceremony, no relationship to declare up front, trivially easy to change what a "car" is made of at runtime (swap in a different engine object, or none at all). Cons: no shared behaviour across "kinds" of object — if ten different object kinds all need a describe()-style method, you're writing (or duplicating) that function ten times, since there's no declared relationship for the language to hang shared method lookup off. Reach for this when objects genuinely just hold other objects as data, with no need for shared behaviour across many similarly-shaped objects.
Class-based single inheritance
A class declares field defaults and methods once; inherits lets a subclass reuse and override them:
class Animal {
field legs = 4
make a function called speak takes self returns msg
return "generic animal noise"
end
}
class Dog inherits Animal {
make a function called speak takes self returns msg
return "woof"
end
}
let a = new("Animal", "a1")
let d = new("Dog", "d1")
print(get(a, "legs"))
print(send(a, "speak"))
print(get(d, "legs"))
print(send(d, "speak"))
Output:
4 generic animal noise 4 woof
Dog inherits legs unchanged from Animal but overrides speak — ordinary method-resolution-order behaviour, verified directly rather than assumed. Pros: a natural fit when objects genuinely form an "is-a" hierarchy and most subclasses share the bulk of a parent's behaviour, overriding only what's different. Cons: single inheritance only — there's no way to inherit from two unrelated parents, and a deep hierarchy can make it hard to see where a given method actually comes from without tracing the whole chain. Reach for this when there's a genuine, single, natural specialization axis (a Dog really is a kind of Animal, not a grab-bag of unrelated capabilities).
Composable traits (mixins)
A trait is just an ordinary class with no instances of its own, referenced by name in another class's traits line — for capabilities that cut across an inheritance hierarchy rather than fitting neatly into it:
class Nameable {
make a function called label takes self returns msg
return "name:" + get(self, "name")
end
}
class Serializable {
make a function called to_json takes self returns msg
return "{\"id\":\"placeholder\"}"
end
}
class Widget {
traits Nameable, Serializable
field visible = 1
}
let w = new("Widget", "w1")
send(w, "set", "name", "button")
print(send(w, "label"))
print(send(w, "to_json"))
print(get(w, "visible"))
Output:
name:button
{"id":"placeholder"}
1
Widget gets both label (from Nameable) and to_json (from Serializable) without either trait needing to be related to Widget or to each other by inheritance. On collision, the last-listed trait wins — confirmed directly, not assumed:
class LoudGreeter {
make a function called greet takes self returns msg
return "HELLO."
end
}
class QuietGreeter {
make a function called greet takes self returns msg
return "...hi"
end
}
class PersonA { traits LoudGreeter, QuietGreeter }
class PersonB { traits QuietGreeter, LoudGreeter }
print(send(new("PersonA", "pa1"), "greet"))
print(send(new("PersonB", "pb1"), "greet"))
Output:
...hi HELLO.
PersonA lists QuietGreeter last and gets its greet; PersonB lists LoudGreeter last and gets its greet instead — same two traits, different winner, purely from list order. Pros: lets unrelated classes share a capability (serialization, naming, logging) without forcing them into a common ancestor just to get it. Cons: the last-listed-wins collision rule is simple but silent — if two traits define the same method and you don't notice the ordering, you get whichever one happens to be listed last with no warning. Order the traits line deliberately, and keep colliding method names rare. Reach for this when a capability is genuinely orthogonal to a class's position in any inheritance hierarchy — most things named "-able" (Serializable, Comparable, Nameable) are this shape.
The mix: inheritance, traits, and composition together
These aren't exclusive — a real class can inherit from a parent, mix in a trait, and hold a composed object, all at once:
class Serializable {
make a function called to_json takes self returns msg
return "{\"kind\":\"" + get(self, "kind") + "\"}"
end
}
class Vehicle {
field kind = "vehicle"
make a function called describe takes self returns msg
return "a " + get(self, "kind")
end
}
class Car inherits Vehicle {
traits Serializable
field kind = "car"
}
let engine = new("Engine", "e1")
send(engine, "set", "horsepower", 300)
let car = new("Car", "c1")
send(car, "set", "engine", engine)
print(send(car, "describe"))
print(send(car, "to_json"))
print(get(get(car, "engine"), "horsepower"))
Output:
a car
{"kind":"car"}
300
Car overrides its inherited kind field (so the inherited describe method correctly says "a car", not "a vehicle"), gains to_json from a trait it shares with unrelated classes, and holds a composed engine object with no declared relationship to either the inheritance chain or the trait — three independent mechanisms, each doing the job it's actually suited to, in one class. Verified identically across the interpreter, the native-compiled path, and the self-hosted compiler.
Choosing between them
- Plain composition when there's no shared behaviour to name — just data holding other data.
- Inheritance when there's one genuine "is-a" specialization axis and subclasses share most of a parent's behaviour.
- Traits when a capability cuts across whatever inheritance hierarchy already exists, or there isn't one.
- All three together is normal, not a code smell in PatLang specifically —
classis deliberately optional sugar over the same object store the plain primitives use, so mixing styles within one object doesn't fight the language the way it might in a stricter single-paradigm OO system.
See also
- Paradigms Guide's OO section — the syntax and resolution-order reference this page builds on.
- The Journey of Building PatLang, Continued Yet Again — how this feature was designed and built, in 5 verified slices.
- Capabilities & Honest Limitations — what this system deliberately doesn't do (no open classes, no
method_missing-style dynamic dispatch, no multiple inheritance).