Perspectives
A perspective is a program-wide, live-rebindable contract slot. Loci program against the contract’s method signatures and bus subscriptions through perspective(Name), while the deployment designates a concrete locus that serves the contract—and can hot-swap the active implementation at runtime via reperspective while preserving live state.
perspective Router { fn route(code: Int) -> Int; fn health() -> Int;}
locus RouterV1 : serves Router { fn route(code: Int) -> Int { return code + 100; } fn health() -> Int { return 1; }}
locus Gateway { params { router: perspective(Router) = RouterV1 { }; } fn handle(code: Int) -> Int { return self.router.route(code); }}Interface or Perspective
Section titled “Interface or Perspective”Hale provides three distinct boundary mechanisms depending on whether you are decoupling method callers, swapping a singleton subsystem, or exposing child state to a parent supervisor:
| Construct | Scope & Cardinality | Dispatch & Representation | Primary Use Case |
|---|---|---|---|
perspective |
Single program-global slot shared by all holders of perspective(P) |
{ data, vtable } slot; 1 pointer load + 1 indirect call; supports live vtable hot-swap via reperspective |
Subsystem seams (routers, storage engines, policies) configured at assembly or hot-swapped in place. |
interface |
Per-value fat pointer; many implementations can coexist simultaneously | Structural satisfaction (any locus with a superset of methods satisfies I, no serves clause needed) |
Heterogeneous collections, per-instance plugins, polymorphic accept(j: Job) parent hooks. |
contract { expose; consume; } |
Parent-child depth edge (H4/H5) |
Direct static field access checked between a parent’s accept(c: Child) and Child |
Declaring which internal fields a child locus exposes to its supervising parent. See Parents & Children. |
Declaring & Serving a Perspective
Section titled “Declaring & Serving a Perspective”The Contract (perspective)
Section titled “The Contract (perspective)”A perspective declaration lists bodyless method signatures (and optional bus subscriptions) that form the contract’s ABI:
perspective Router { fn route(code: Int) -> Int; fn health() -> Int;}The Implementation (serves)
Section titled “The Implementation (serves)”A locus implements a perspective by adding : serves <Perspective> to its header and providing every method and bus subscription declared by the perspective:
locus RouterV1 : serves Router { fn route(code: Int) -> Int { return code + 100; } fn health() -> Int { return 1; }}- Structural signature verification: Parameter types, return types, and
fallibleannotations must match the perspective declaration exactly. Missing methods or mismatched signatures fail at compile time at theservesclause. - Combining with annotations:
servesshares the post-colon header list with locus annotations (e.g.,locus RouterV1 : serves Router, tier 2). - Cross-seed perspectives: A locus can serve a perspective imported from another library seed using a qualified path:
- Header:
locus RouterV1 : serves lib::Router - Parameter type:
perspective(lib::Router) - Live swap:
reperspective self.router as lib::RouterV2;
- Header:
Holding, Designating, and Calling a Slot
Section titled “Holding, Designating, and Calling a Slot”Holders declare a parameter of type perspective(Router) (using a bare Router as a type is a compile error):
- Designating the implementation: Providing a default or override initializer (
router: perspective(Router) = RouterV1 { };orGateway { router: RouterV2 { } }) constructs the implementation and binds the program-globalRouterslot to it. - Overriding at assembly: A parent or
main locuscan substitute any locus thatserves Routerwhen constructing the holder:
locus RouterV2 : serves Router { fn route(code: Int) -> Int { return code + 200; } fn health() -> Int { return 1; }}
main locus App { params { gw: Gateway = Gateway { router: RouterV2 { } }; }}
fn main() { App { }; }- Holding without designating: Omitting the default initializer (
params { router: perspective(Router); }) binds the field to the program’s existingRouterslot without constructing a new instance or re-pointing the slot. This allows dynamically spawned children to call through the implementation designated bymain:
locus Attempt { params { router: perspective(Router); } // holds the global slot without re-designating run() { println(self.router.route(1)); }}
locus Work { accept(a: Attempt) { } run() { Attempt { }; Attempt { }; } // both calls route through RouterV2}
main locus App { params { router: perspective(Router) = RouterV2 { }; // designates RouterV2 for the program w: Work = Work { }; }}Slot Constraints & Diagnostics
Section titled “Slot Constraints & Diagnostics”- Program-global singleton slot: Every
perspective(Router)handle in the binary references the same underlying slot. If multiple holders designate different implementations at construction, the most recently constructed designation wins program-wide. - Must be designated before use:
hale checkrejects any program where a locus holdsperspective(P)with no initializer unless at least one constructed locus designatesP. At runtime, calling through an undesignated slot (for example, from an earlier sibling parameter born before the designating parameter) aborts the process naming the perspective. - Methods-only encapsulation: Callers can only invoke methods declared on the
perspective. Direct field reads or writes through the slot (self.router.hits = 0;) are rejected at compile time. - No
falliblecalls through slots: While aperspectivesignature andservesimplementation must agree onfallible, calling afalliblemethod through aperspective(P)slot is currently rejected at compile time because perspective dispatch does not propagate a value-error channel.
Live Redeployment (reperspective)
Section titled “Live Redeployment (reperspective)”The reperspective statement hot-swaps the active implementation behind a perspective slot at runtime with a single vtable pointer store:
perspective Router { fn route(code: Int) -> Int; fn health() -> Int;}locus RouterV1 : serves Router { fn route(code: Int) -> Int { return code + 100; } fn health() -> Int { return 1; }}locus RouterV2 : serves Router { fn route(code: Int) -> Int { return code + 200; } fn health() -> Int { return 1; }}
locus Gateway { params { router: perspective(Router) = RouterV1 { }; } run() { println(self.router.route(1)); // prints 101 (RouterV1) reperspective self.router as RouterV2; println(self.router.route(1)); // prints 201 (RouterV2) }}
fn main() { Gateway { }; }reperspective Rules
Section titled “reperspective Rules”- Ownership authority:
reperspective self.<field> as NewImpl;can only be executed by a locus that holds the perspective slot inself.<field>, andNewImplmust declare: serves <Perspective>. - Live state preservation & identical footprint: A perspective slot stores
{ data, vtable }. Executingreperspectiveswaps only thevtablepointer, leavingdatauntouched so the new implementation continues running on the live state accumulated by the previous implementation:To guarantee memory layout compatibility across swaps, every locus thatself.counter.bump();self.counter.bump(); // count == 2 under CounterV1reperspective self.counter as CounterV2; // swaps vtable onlyprintln(self.counter.get()); // still 2 under CounterV2servesa given perspective must declare an identicalparamsfootprint (same field names and types). Adding, removing, or retyping a parameter field across versions is a compile error. - Zero-allocation swap: Nothing is re-allocated,
birth()is not re-run, anddissolve()is not called duringreperspective.
Bus Subscriptions in Perspectives
Section titled “Bus Subscriptions in Perspectives”A perspective can include a bus { } block alongside its synchronous methods so that both synchronous calls and asynchronous bus deliveries are governed by the contract:
type Order { id: Int; }
perspective OrderRouter { fn health() -> Int; bus { subscribe "orders" as on_order of type Order; }}- Structural bus checking: Any locus that
: serves OrderRoutermust declare the matchingbus { subscribe "orders" as on_order of type Order; }entry and handler method. - Live bus handler re-pointing: Executing
reperspective self.router as OrderV2;tombstones the previous implementation’s bus subscriptions on the shared slot state and registersOrderV2’s handlers over the same state:"orders" <- Order { id: 1 }; // dispatched to OrderV1::on_orderreperspective self.router as OrderV2; // hot-swaps sync vtable and bus handlers"orders" <- Order { id: 2 }; // dispatched to OrderV2::on_order - Deterministic swap boundary: Because cooperative bus publishing captures the target handler at the moment
<-executes, messages published beforereperspectiveare processed by the old handler and messages published afterreperspectiveare processed by the new handler.
Next: Parents & Children.