What Are Selectors and How Does Dispatch Work in Maru?
In the Maru programming language, selectors implement single-dispatch polymorphism by using the first argument's type to index into a method table, with automatic fallback to superclass methods and default handlers when specific implementations are missing.
Maru is a self-hosting Lisp dialect developed by Attila Lendvai that emphasizes minimalism and extensibility. At the heart of its object system lies the selector, a mechanism that enables dynamic method dispatch based on runtime types. Understanding how selectors and dispatch work in Maru is essential for extending the language and building efficient, polymorphic abstractions.
Understanding Selectors in Maru
A selector in Maru is a first-class object that manages a collection of methods indexed by type. Unlike generic functions that might dispatch on multiple arguments, selectors perform single-dispatch—they choose an implementation based solely on the type of the first argument.
The Selector Record Structure
The selector abstraction is defined as a record type <selector> in source/selector.l (lines 10-15). According to the Maru source code, this record contains three critical fields:
name– A symbolic identifier for the selector (e.g.,+,print,eval)methods– An expandable array where each index corresponds to a type-id; slots contain the specific method implementation for that typedefault– A fallback function invoked when no specific method or inherited method exists for a given type
This structure enables O(1) lookup for directly implemented methods while supporting inheritance through the fallback chain.
How Dispatch Works in Maru
When you invoke a selector in Maru, the system executes a specific dispatch algorithm defined in the apply method for <selector> objects (source/selector.l, lines 26-40).
The Dispatch Algorithm
The dispatch process follows these precise steps as implemented in the Maru evaluator:
- Extract the first argument using
caron the argument list - Compute the type identifier:
- During bootstrapping, Maru uses
type-id-offor fast lookup - In normal operation, it retrieves the full type-id via
<type>-idapplied to(type-of …)
- During bootstrapping, Maru uses
- Array lookup – The system calls
array-atto fetch the method stored at the computed type-id index in the selector'smethodsarray - Hierarchy traversal – If the array slot is empty, Maru invokes
selector/lookup-effective-method(lines 16-24) which walks the type hierarchy using<record>-superlinks to find an inherited method - Default fallback – If no method exists in the hierarchy, the selector's
defaultfunction is applied - Execution – The selected method (or default) is finally applied to the original argument list
Thus, the complete dispatch path is: direct array slot → superclass walk → default handler.
Method Lookup and Inheritance
The selector/lookup-effective-method function enables polymorphic behavior by supporting inheritance. When a specific type lacks a method implementation, Maru traverses the superclass chain via the <record>-super field until it finds a matching method or exhausts the hierarchy.
This design allows developers to define methods on base types (like <object>) that automatically apply to derived types unless overridden, following standard object-oriented inheritance patterns.
Defining and Adding Methods
Maru provides macros that simplify selector creation and method registration, all defined in source/selector.l.
Creating Selectors with define-selector
The define-selector macro (lines 57-68) creates a fresh <selector> record with the specified name and an empty method table. If invoked with a name that already exists, it ensures the selector object is properly initialized.
Adding Methods with define-method
Methods are registered using selector/add-method (lines 47-55), typically invoked through the define-method macro. The syntax follows:
(define-method <selector> <type> (arguments…) body…)
This expands to a call that stores the method implementation in the selector's method array at the index corresponding to <type>'s type-id.
Practical Example
Here is a complete example demonstrating selector definition and dispatch:
;; Define a selector for serialization
(define-selector serialize)
;; Method for integers
(define-method serialize <integer> (n)
(integer-to-string n))
;; Method for lists
(define-method serialize <list> (lst)
(list "[" (join (map serialize lst) ", ") "]"))
;; Default method for unsupported types
(define-method serialize <object> (obj)
(error "Cannot serialize object of type" (type-of obj)))
;; Usage
(serialize 42) ; → "42"
(serialize '(1 2 3)) ; → "[1, 2, 3]"
Selectors vs. Multimethods in Maru
While selectors provide single-dispatch polymorphism, Maru also supports multimethods through source/generic.l. Multimethods dispatch based on the types of all arguments rather than just the first, enabling multiple-dispatch polymorphism similar to CLOS (Common Lisp Object System).
Selectors remain the foundational mechanism used throughout Maru's core runtime for built-in operations like printing, arithmetic, and collection access, while multimethods offer additional flexibility for complex polymorphic scenarios.
Summary
- Selectors in Maru provide single-dispatch polymorphism based on the first argument's type, implemented as
<selector>records insource/selector.l. - The dispatch mechanism uses a type-indexed array for O(1) lookup, falling back to superclass traversal via
selector/lookup-effective-methodand finally to a default handler. - Method registration occurs through
define-method, which expands toselector/add-methodto populate the dispatch table. - Inheritance is supported through the
<record>-superchain, allowing methods defined on base types to apply to derived types. - Multimethods in
source/generic.loffer multiple-dispatch as an alternative to single-dispatch selectors.
Frequently Asked Questions
What is the difference between a selector and a generic function in Maru?
Selectors perform single-dispatch on the first argument only, while generic functions (defined in source/generic.l) support multiple-dispatch by considering the types of all arguments. Selectors are the foundational mechanism used throughout Maru's core runtime for built-in operations, whereas generic functions provide CLOS-style multiple-dispatch for complex polymorphic scenarios.
How does Maru handle method lookup when a type has no direct implementation?
When a selector's method array lacks an entry for a specific type-id, Maru invokes selector/lookup-effective-method (defined in source/selector.l, lines 16-24) to traverse the type hierarchy. This function follows <record>-super links to check ancestor types for method implementations, enabling inheritance of methods from parent types. Only if the entire hierarchy is exhausted does the selector invoke its default handler.
Can selectors be modified at runtime in Maru?
Yes, selectors are mutable records with expandable method arrays. You can dynamically add or replace methods using selector/add-method or the define-method macro without recompiling the selector definition. This runtime extensibility allows programs to extend the behavior of existing selectors for new types, supporting a flexible, open-system design pattern.
Where is the selector dispatch logic implemented in the Maru source code?
The core dispatch algorithm resides in source/selector.l between lines 26-40, specifically within the apply method defined for <selector> records. The type hierarchy traversal for inherited methods is handled by selector/lookup-effective-method at lines 16-24 in the same file, while method registration occurs through selector/add-method at lines 47-55.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →