Classes
Mux provides object-oriented programming through classes and interfaces (traits).
Basic Class Definition
Key Points:
- Fields must have explicit types (no
autoinference) - Methods use
selfto access instance fields - Methods follow same rules as regular functions
Class Instantiation
Classes use the .new() method pattern:
Design Note: Mux uses explicit .new() rather than direct constructor calls to distinguish class instantiation from function calls and enum variant construction. The .new() method will always instantiate a new object with all default "zero" values for fields, and then you can set fields afterward. This is a simple and consistent pattern for object creation.
The Mux style for constructors is to use a common factory method (see below) that creates and initializes the object. Name factories by behavior (from(...), from_<source>(...), with_<feature>(...)) rather than new.
Interfaces (Traits)
Interfaces define required methods that classes must implement:
Implementing Interfaces
Use the is keyword to implement interfaces:
Note: Use is instead of implements (like Java). Multiple interfaces separated by commas.
Methods
Instance Methods
Access instance data via self:
Methods with Unused Parameters
Static Methods with common
The common keyword declares static (class-level) methods:
common vs const
| Keyword | Purpose | Usage |
|---|---|---|
common | Static methods and factory functions | ClassName.method() |
const | Immutable constants | const int MAX = 100 |
Key Differences:
- Instance methods (no keyword) operate on
selfand require an instance - Static methods (
common) have noselfand are called on the class - Const fields are immutable instance/class fields, not methods
- Static methods cannot access instance fields (no
selfcontext)
Constants in Classes
Classes can have constant (immutable) fields:
Const Enforcement:
- Cannot reassign:
self.MAX_RETRIES = value-> ERROR - Cannot increment/decrement:
self.MAX_RETRIES++-> ERROR - Use
constfor fields that shouldn't change after initialization
Generic Classes
Classes can be generic over type parameters:
See Generics for more details.
Built-in Capabilities
Four capabilities are built into the language rather than declared. A class opts in by naming one and writing the method it requires, and the operators then work on that class:
| Capability | Method | What it enables |
|---|---|---|
Equatable | eq(Self) returns bool | == and != |
Comparable | cmp(Self) returns int | <, <=, >, >=, and == |
Hashable | hash() returns int and eq | use as a map key or set member |
Stringable | to_string() returns string | to_string() on the class |
cmp returns negative, zero or positive like C's strcmp, so one method
supplies every ordering operator. Comparable and Hashable each grant
Equatable, so a class needs only the one that fits - and Hashable requires
eq as well as hash, because a hash alone cannot tell two keys in one bucket
apart.
A class declaring Hashable can key a map, and instances match by their fields
rather than by identity:
A generic class cannot declare Equatable, Comparable or Hashable:
those are registered with the runtime once per class, and a generic class
shares one registration across every instantiation. Stringable registers
nothing and is available to generic classes.
Interface Dispatch
Mux supports both static interface bounds and owned dynamic interface values.
Use a type parameter when the concrete type is known at compile time. Use
dyn<Interface> when a value must cross a function boundary without exposing
its concrete class.
An owned dynamic interface value stores the concrete object and its interface
dispatch table. Passing a class to dyn<Interface> performs the checked
conversion. Calling a method uses the table, so different classes can be
handled by one function:
Dynamic interface values own their erased object. They can be stored in
heterogeneous collections through the dynamic interface type. Generic dynamic
interfaces such as dyn<T> are not supported; name a declared interface.
Why static dispatch?
- Zero cost: No pointer indirection, direct function calls
- Inlining: LLVM can inline interface methods
- Optimization: Better branch prediction, no indirect jumps
Static interface bounds still cannot be added to types from other modules. Use a named dynamic interface when a heterogeneous collection or erased function argument is required. Model a closed set of alternatives as an enum when dynamic dispatch is not needed.
Building a Class From a Document
Document conversion is explicit Mux code. A class receives no generated
serializer, deserializer, field rename, omit, flatten, or default mapping.
Implement JsonRepresentable or CsvRepresentable when the class should
provide the corresponding operation. Parser methods such as from_json and
from_csv are ordinary user-written methods.
The name says what shape it returns:
| Method | Returns |
|---|---|
Config.to_json() | result<Json, JsonError> - explicit class serialization |
Config.from_json(text) | result<Config, JsonError> - explicit class parsing |
Config.list_from_json(text) | result<list<Config>, JsonError> - only when explicitly defined |
Config.to_csv() | result<string, CsvError> - explicit class serialization |
Config.from_csv(text) | result<Config, CsvError> - only when explicitly defined |
There is no compiler-provided singular or list parser. A CSV document is a table, but a program may define either shape when its input contract requires it.
A common parser contract
The compiler does not impose these rules. They are a useful contract for an explicit parser that wants the same behavior as the standard shape fixtures:
| Case | Result |
|---|---|
| A required field is missing | error naming the field |
An optional<T> field is missing | none |
An optional<T> field is null | none |
| A field is the wrong kind | error naming the field and the type expected |
| The document has fields you did not declare | ignored |
Absence and an explicit null deliberately mean the same thing. Extra fields
are ignored so a server adding one does not break a program that reads it.
Shapes it understands
An error inside a nested class names the field that was actually wrong, not the
one that contained it - missing required field 'qty', not "bad shipping".
Data whose shape you cannot declare
JSON allows a heterogeneous array - [1, "two", true] is valid - and no Mux
type holds those together. Declare such a field as Json and read each entry on
its own terms:
This is the escape hatch, and the reason Json accessors
still exist: everything with a knowable shape goes through a class, and the rest
goes through Json.
CSV is different
A CSV cell is always text, so 3 in a file is the string "3". An int
column is therefore parsed rather than type-checked, and an unparseable cell
names the column:
column 'qty': expected an intFor the same reason a CSV column can only be a string, int, float, bool
or an optional of those - a nested class cannot come out of a single cell. And
optional there means the column may be absent from the header; within a
row that has the column, an empty cell is an empty string, because CSV cannot
tell "empty" from "absent" once the column exists.
Best Practices
- Fields must be explicitly typed - No
autofor class fields - Use interfaces for polymorphism - Define common behavior
- Use
commonfor factory methods - Create instances with pre-populated data - Keep classes focused - Single responsibility principle
- Use
constfor immutable fields - Prevent accidental modification - Use generic classes - Reusable data structures
- Prefer static dispatch - Better performance than dynamic dispatch
See Also
- Generics - Generic classes and type parameters
- Interfaces - Built-in interfaces for common operations
- Memory - Reference counting and object lifecycle
- Functions - Method definitions