You just have to learn 4 things to know Object-Spatial Programming#
Most programming paradigms ask you to learn dozens of concepts, patterns, and best practices. Object-Spatial Programming (OSP) is different. At its core, there are really just four fundamental concepts you need to understand. Master these four things, and you'll have unlocked an entirely new way of thinking about computation—one that's particularly well-suited for modern applications dealing with complex relationships, graph structures, and distributed data.
This article will teach you the complete model of Object-Spatial Programming through these four conceptual buckets. Each builds naturally on the last, and by the end, you'll understand why OSP represents a genuine paradigm shift in how we structure programs.
What is Object-Spatial Programming?#
Before we dive into the four concepts, let's establish what makes Object-Spatial Programming fundamentally different from traditional Object-Oriented Programming (OOP).
Traditional OOP:
Object-Spatial Programming:
Data is arranged in space → You send computation to travel through the data
(Walkers visit nodes in a graph)
The Restaurant Analogy#
Think of it this way:
- OOP: You're at home, you call restaurants and have food delivered to you
- OSP: You send a robot out to visit different restaurants and collect food
Both get you fed, but they work fundamentally differently! In OSP, computation is mobile and data is spatial.
Concept 1: The Spatial Object Model (Nodes & Edges)#
The first thing you need to understand is how objects are structured in Object-Spatial Programming. Instead of thinking about isolated objects, you think about objects arranged in space with explicit relationships.
🔑 Core Insight: Everything in OSP is built on classes.
node,edge, andwalkerare not new primitive types—they're classes that inherit all OOP capabilities (methods, properties, inheritance, polymorphism) and add spatial/traversal semantics. This means OSP extends OOP rather than replacing it.
The Two Pillars of the Spatial Object Model#
| Type | What It Is | Purpose | Traditional Analogy |
|---|---|---|---|
node |
Class with spatial semantics | Data locations that can be connected in graphs | Houses on a map |
edge |
Class representing relationships | First-class connections between nodes | Roads between houses |
Key point: Nodes and edges are full-featured classes with methods, inheritance, and properties—PLUS spatial connection capabilities.
Creating Nodes#
Important: Nodes are classes! They have all the semantics of regular classes (methods, inheritance, polymorphism) PLUS the ability to be connected in spatial graphs.
node Person {
has name: str;
has age: int;
# Regular method - just like any class
def greet -> str {
return f"Hello, I'm {self.name}!";
}
def celebrate_birthday {
self.age += 1;
print(f"{self.name} is now {self.age}!");
}
}
with entry {
alice = Person(name="Alice", age=25);
bob = Person(name="Bob", age=30);
# Use like regular objects
print(alice.greet());
alice.celebrate_birthday();
}
At this point, nodes work exactly like regular classes. The spatial magic happens when we connect them...
Connecting Nodes: The Spatial Operators#
This is where OSP diverges from traditional OOP. You can directly connect nodes using spatial operators:
node Person {
has name: str;
}
with entry {
alice = Person(name="Alice");
bob = Person(name="Bob");
charlie = Person(name="Charlie");
# Build the graph structure
alice ++> bob; # Alice → Bob
alice ++> charlie; # Alice → Charlie
bob ++> charlie; # Bob → Charlie
# Connect Alice to global root
root ++> alice;
print([alice -->]);
}
Visual representation:
graph TD
Alice --> Bob
Alice --> Charlie
Bob --> Charlie
Connection Operators Reference#
| Operator | Direction | Example | Result |
|---|---|---|---|
++> |
Forward | alice ++> bob; |
alice → bob |
<++ |
Backward | alice <++ bob; |
bob → alice |
<++> |
Both ways | alice <++> bob; |
alice ↔ bob |
Typed Edges: First-Class Relationships#
Here's what makes OSP truly revolutionary: relationships are first-class objects with their own types and properties!
Important: Edges are also classes! Like nodes, edges have all the semantics of regular classes—they can have methods, properties, and behavior—PLUS they represent connections in the graph.
node Person {
has name: str;
}
edge Friend {
has since: int; # Year they became friends
has strength: int = 5; # Closeness rating 1-10
# Regular method - edges can have behavior!
def years_of_friendship(current_year: int) -> int {
return current_year - self.since;
}
def is_strong -> bool {
return self.strength >= 7;
}
}
edge Coworker {
has department: str;
def same_department(other_dept: str) -> bool {
return self.department == other_dept;
}
}
with entry {
alice = Person(name="Alice");
bob = Person(name="Bob");
charlie = Person(name="Charlie");
# Different types of relationships with their own data
alice +>:Friend(since=2015, strength=9):+> bob;
alice +>:Coworker(department="Engineering"):+> charlie;
# Connect to root for visualization
root ++> alice;
# Query different edge types
friends = [alice ->:Friend:->];
coworkers = [alice ->:Coworker:->];
print(f"Alice has {len(friends)} friend(s) and {len(coworkers)} coworker(s)");
}
This is a paradigm shift! In traditional OOP, relationships are just data in lists or dictionaries. In OSP, relationships are classes in their own right that exist in the spatial model and can have methods, properties, and behavior.
graph LR
Alice -->|Friend since 2015<br/>strength: 9| Bob
Alice -->|Coworker<br/>dept: Engineering| Charlie
Querying the Graph: Edge References#
Once you've built a graph structure, you can query it using edge references with square brackets:
node Person {
has name: str;
}
edge Friend {
has since: int;
}
with entry {
alice = Person(name="Alice");
bob = Person(name="Bob");
charlie = Person(name="Charlie");
root ++> alice;
alice +>:Friend(since=2015):+> bob;
alice +>:Friend(since=2020):+> charlie;
# Query all connections
all_connections = [alice -->];
print(f"Alice has {len(all_connections)} connections");
# Query specific edge types
friends = [alice ->:Friend:->];
print(f"Alice has {len(friends)} friends");
# Query with filters on edge properties
old_friends = [alice ->:Friend:since < 2018:->];
print(f"Alice has {len(old_friends)} friends since before 2018");
}
Edge Reference Patterns#
| Pattern | Direction | Type Filter | Example |
|---|---|---|---|
[node -->] |
Outgoing | Any | All outgoing connections |
[node <--] |
Incoming | Any | All incoming connections |
[node <-->] |
Both | Any | All connections |
[node ->:Type:->] |
Outgoing | Specific | Specific edge type |
[node ->:Type:property > 5:->] |
Outgoing | Filtered | With property filter |
Why This Matters: Traditional vs Spatial#
Traditional OOP approach:
# Verbose, imperative, error-prone
obj Person {
has name: str;
has connections: list = [];
}
obj Connection {
has conn_type: str;
has target: object;
}
def find_friends(person: Person) -> list {
friends = [];
for connection in person.connections {
if connection.conn_type == "friend" {
friends.append(connection.target);
}
}
return friends;
}
with entry {
alice = Person(name="Alice");
bob = Person(name="Bob");
conn = Connection(conn_type="friend", target=bob);
alice.connections.append(conn);
friends = find_friends(alice);
print(f"Alice's friends: {len(friends)}");
}
OSP approach:
Key advantages: 1. Natural representation: Relationships are explicit in the structure 2. Type safety: Edges have their own types and properties 3. Declarative queries: Express what you want, not how to get it 4. Visual clarity: The code structure mirrors the data structure
Nodes and Edges Support Full OOP#
Since nodes and edges are classes, they support all standard OOP features:
# Base node with common behavior
node Entity {
has id: str;
has created_at: str;
def get_age_days(current_date: str) -> int {
# Common method inherited by all entities
return 100; # Simplified
}
}
# Specialized nodes via inheritance
node Person(Entity) {
has name: str;
has email: str;
def send_notification(message: str) {
print(f"Sending to {self.email}: {message}");
}
}
node Organization(Entity) {
has company_name: str;
has industry: str;
def get_display_name -> str {
return f"{self.company_name} ({self.industry})";
}
}
# Edge with inheritance
edge Relationship {
has start_date: str;
def is_active -> bool {
return True; # Base implementation
}
}
edge Employment(Relationship) {
has role: str;
has salary: float;
def get_description -> str {
return f"{self.role} since {self.start_date}";
}
}
with entry {
alice = Person(id="1", created_at="2020-01-01", name="Alice", email="alice@example.com");
acme = Organization(id="2", created_at="2015-01-01", company_name="Acme Corp", industry="Tech");
# Create typed edge with inheritance
root ++> alice;
alice +>:Employment(start_date="2020-06-01", role="Engineer", salary=120000.0):+> acme;
# Use inherited and specific methods
print(alice.get_age_days("2024-01-01")); # Inherited from Entity
alice.send_notification("Welcome!"); # Specific to Person
print(acme.get_display_name()); # Specific to Organization
# Query connections
jobs = [alice ->:Employment:->];
print(f"Alice has {len(jobs)} job(s)");
}
The key insight: node, edge, and walker are not restrictions—they're extensions of regular classes that add spatial semantics while preserving all OOP capabilities.
Concept 2: The Mobile Computation Model (Walkers)#
Now that you understand how objects are structured spatially, the second concept is about how computation moves through that space. This is where walkers come in.
What is a Walker?#
A walker is a mobile unit of computation that travels through the graph, visiting nodes and performing actions. Think of it as an autonomous agent that navigates your object graph.
Important: Walkers are also classes! They have all the semantics of regular classes (state, methods, inheritance) PLUS the ability to move through the graph and trigger abilities based on what they visit.
node Person {
has name: str;
}
walker Greeter {
# Walker state - like class attributes
has greeting_count: int = 0;
# Regular methods work too!
def get_stats -> str {
return f"Greeted {self.greeting_count} people";
}
# Spatial ability - triggers when visiting root
can start with `root entry {
print("Walker starting journey!");
visit [-->]; # Visit all connected nodes
}
# Spatial ability - triggers when visiting Person nodes
can greet with Person entry {
print(f"Hello, {here.name}!");
self.greeting_count += 1; # Update walker state
visit [-->]; # Continue to this person's connections
}
}
with entry {
# Build graph
alice = Person(name="Alice");
bob = Person(name="Bob");
charlie = Person(name="Charlie");
root ++> alice ++> bob ++> charlie;
# Spawn walker at root
greeter = Greeter();
root spawn greeter;
# Access walker state after traversal
print(greeter.get_stats());
}
Output:
Walker Anatomy#
walker MyWalker {
# Walker state (like object attributes)
has counter: int = 0;
has visited_names: list = [];
# Abilities define what happens at different nodes
can ability_name with NodeType entry {
# Your logic here
}
}
The Visit Statement#
The visit statement is how walkers navigate the graph. It tells the walker where to go next.
walker Explorer {
can explore with Person entry {
print(f"Visiting {here.name}");
# Different visit patterns:
visit [-->]; # Visit all outgoing connections
# visit [<--]; # Visit all incoming connections
# visit [<-->]; # Visit all connections (both directions)
# visit [->:Friend:->]; # Visit only via Friend edges
}
}
Visit Pattern Reference#
| Pattern | Meaning |
|---|---|
visit [-->]; |
Visit all nodes connected via outgoing edges |
visit [<--]; |
Visit all nodes connected via incoming edges |
visit [<-->]; |
Visit all nodes connected in any direction |
visit [->:Type:->]; |
Visit only via specific edge type |
visit [->:Type:property > 5:->]; |
Visit via filtered edges |
visit node_list; |
Visit specific nodes in a list |
Special References in Walkers#
When writing walker code, you have access to special references:
| Reference | Meaning | Use Case |
|---|---|---|
self |
The walker itself | Access walker state |
here |
The current node being visited | Access node properties |
root |
The global root node | Always available anchor point |
Example: Finding a Specific Node#
node Person {
has name: str;
}
walker FindPerson {
has target: str;
has found: bool = False;
can start with `root entry {
visit [-->];
}
can search with Person entry {
if here.name == self.target {
print(f"Found {here.name}!");
self.found = True;
disengage; # Stop immediately (we'll cover this in Concept 4)
}
visit [-->];
}
}
with entry {
alice = Person(name="Alice");
bob = Person(name="Bob");
charlie = Person(name="Charlie");
root ++> alice ++> bob ++> charlie;
finder = FindPerson(target="Bob");
root spawn finder;
print(f"Search result: {finder.found}");
}
graph TD
Root[Root] -->|1. spawn| Alice[Person Alice]
Alice -->|2. visit| Bob[Person Bob]
Bob[Person Bob - FOUND!]
Bob -.->|3. disengage| Charlie[Person Charlie<br/>not visited]
style Bob fill:#90EE90
style Charlie fill:#FFB6C6
Why Mobile Computation Matters#
Traditional programming brings data to computation:
# Traditional approach
def process_all(data_list):
for item in data_list:
# Computation happens here
process(item)
OSP sends computation to data:
# OSP approach
walker Processor {
can process with DataNode entry {
# Computation happens AT the data
visit [-->];
}
}
This shift enables: - Natural graph traversal: No manual iteration through connections - Automatic dispatching: Right code runs at right nodes - Distributed potential: Walkers can move across machine boundaries - Declarative navigation: Express intent, not mechanics
Concept 3: The Event-Driven Interaction Model (Abilities)#
The third concept is understanding how walkers and nodes interact. This happens through abilities—automatic methods that trigger when certain events occur.
What are Abilities?#
Abilities are methods that automatically execute based on type matching between walkers and nodes. They define the interaction protocol between mobile computation and spatial data.
Ability Triggers#
| Trigger | When it Executes |
|---|---|
with NodeType entry |
When walker enters a node of type NodeType |
with NodeType exit |
When walker leaves a node of type NodeType |
with WalkerType entry |
When a walker of type WalkerType visits (node perspective) |
with \root entry` |
When walker spawns at the root node |
Walker Abilities: The Walker's Perspective#
These abilities define what the walker does when visiting different node types:
node Person {
has name: str;
has age: int;
}
node City {
has name: str;
has population: int;
}
walker Tourist {
has places_visited: int = 0;
can start with `root entry {
print("Starting tour!");
visit [-->];
}
can meet_person with Person entry {
print(f"Met {here.name}, age {here.age}");
self.places_visited += 1;
visit [-->];
}
can visit_city with City entry {
print(f"Visiting {here.name}, population {here.population}");
self.places_visited += 1;
visit [-->];
}
can finish with exit {
print(f"Tour complete! Visited {self.places_visited} places");
}
}
Node Abilities: The Node's Perspective#
Nodes can also have abilities that define how they react to visiting walkers:
node Person {
has name: str;
has greeting_count: int = 0;
# Node's perspective: "When a Greeter visits me"
can be_greeted with Greeter entry {
self.greeting_count += 1;
print(f"{self.name} has been greeted {self.greeting_count} times");
}
}
walker Greeter {
# Walker's perspective: "When I visit a Person"
can greet with Person entry {
print(f"Hello, {here.name}!");
visit [-->];
}
}
Bidirectional Polymorphism: The Revolutionary Part#
Here's what makes OSP truly unique: both nodes and walkers can define abilities for the same interaction. When a walker visits a node, both abilities execute!
node Person {
has name: str;
# Node's perspective
can greet_visitor with Visitor entry {
print(f"{self.name} says: Welcome, visitor!");
}
}
walker Visitor {
# Walker's perspective
can meet_person with Person entry {
print(f"Visitor says: Hello, {here.name}!");
visit [-->];
}
}
with entry {
alice = Person(name="Alice");
root ++> alice;
root spawn Visitor();
}
Output:
sequenceDiagram
participant W as Visitor Walker
participant P as Person Node (Alice)
Note over W,P: Walker visits Person
W->>P: Triggers Person entry
activate W
activate P
P-->>P: Node ability executes:<br/>"greet_visitor"
W-->>W: Walker ability executes:<br/>"meet_person"
deactivate W
deactivate P
Note over W,P: Both abilities run!
This is bidirectional polymorphism: the interaction is defined from both perspectives. This is unique to OSP and has no direct equivalent in traditional OOP.
Ability Execution Order#
When a walker visits a node, abilities execute in this order:
- Node's ability for the walker type (if it exists)
- Walker's ability for the node type (if it exists)
Execution:
Real-World Example: Social Network#
node User {
has username: str;
has posts_viewed: int = 0;
can track_view with ContentViewer entry {
self.posts_viewed += 1;
print(f"[{self.username}] Post views: {self.posts_viewed}");
}
}
edge Follow {}
walker ContentViewer {
has content_count: int = 0;
can start with `root entry {
visit [-->];
}
can view_content with User entry {
self.content_count += 1;
print(f"[Viewer] Viewing content from {here.username}");
visit [->:Follow:->]; # Only follow edges
}
}
with entry {
alice = User(username="alice");
bob = User(username="bob");
charlie = User(username="charlie");
root ++> alice;
alice +>:Follow:+> bob;
bob +>:Follow:+> charlie;
root spawn ContentViewer();
}
Output:
[Viewer] Viewing content from alice
[alice] Post views: 1
[Viewer] Viewing content from bob
[bob] Post views: 1
[Viewer] Viewing content from charlie
[charlie] Post views: 1
Why Event-Driven Interaction Matters#
| Traditional OOP | OSP with Abilities |
|---|---|
| Explicit method calls | Automatic ability dispatch |
| One-sided interaction | Bidirectional interaction |
| Tight coupling | Loose coupling via types |
| Manual routing | Type-driven routing |
Abilities enable: - Automatic dispatch: Right code runs without explicit routing - Separation of concerns: Walker logic separate from node logic - Extensibility: Add new walker types without changing nodes - Bidirectional contracts: Both sides define interaction
Concept 4: The Traversal & Control Flow (Navigation & Results)#
The fourth and final concept is understanding how to control walker navigation and collect results. This is where you orchestrate the computation across your spatial object graph.
Visit Strategies#
You've seen basic visit statements, but there are sophisticated patterns for controlling traversal:
1. Visit All Connections#
2. Visit Specific Edge Types#
3. Visit with Filters#
# Visit friends from before 2020
visit [->:Friend:since < 2020:->];
# Visit high-strength friendships
visit [->:Friend:strength > 7:->];
4. Multi-Hop Traversal#
# Friends of friends (2 hops)
potential_friends = [here ->:Friend:-> ->:Friend:->];
visit potential_friends;
5. Visit Specific Nodes#
# Build a list and visit it
targets = [here -->];
filtered_targets = [t for t in targets if t.age > 25];
visit filtered_targets;
The Report Statement: Streaming Results#
Unlike return (which stops execution), report sends values back to the caller while the walker continues executing.
node Person {
has name: str;
has age: int;
}
walker AgeCollector {
has ages: list = [];
can start with `root entry {
visit [-->];
}
can collect with Person entry {
self.ages.append(here.age); # Collect in walker state
visit [-->]; # Keep going!
}
}
with entry {
alice = Person(name="Alice", age=25);
bob = Person(name="Bob", age=30);
charlie = Person(name="Charlie", age=28);
root ++> alice ++> bob ++> charlie;
collector = AgeCollector();
root spawn collector;
print(f"Collected ages: {collector.ages}"); # [25, 30, 28]
print(f"Average age: {sum(collector.ages) / len(collector.ages):.2f}"); # 27.67
}
sequenceDiagram
participant Caller
participant Walker as AgeCollector
participant Alice as Person(Alice, 25)
participant Bob as Person(Bob, 30)
participant Charlie as Person(Charlie, 28)
Caller->>Walker: Spawn walker
Walker->>Alice: Visit
Alice-->>Caller: Report 25
Walker->>Bob: Visit
Bob-->>Caller: Report 30
Walker->>Charlie: Visit
Charlie-->>Caller: Report 28
Walker->>Caller: Return [25, 30, 28]
Report vs Return#
| Feature | return |
report |
|---|---|---|
| Stops execution | ✓ Yes | ✗ No |
| Sends value back | ✓ One value | ✓ Multiple values |
| Continues walking | ✗ No | ✓ Yes |
| Result type | Single value | List of values |
The Disengage Statement: Early Exit#
Use disengage to stop the walker immediately, no matter where it is:
node Person {
has name: str;
}
walker FindPerson {
has target: str;
has found: bool = False;
can start with `root entry {
visit [-->];
}
can search with Person entry {
if here.name == self.target {
print(f"Found {here.name}!");
self.found = True;
disengage; # Stop immediately!
}
visit [-->];
}
}
with entry {
alice = Person(name="Alice");
bob = Person(name="Bob");
charlie = Person(name="Charlie");
dana = Person(name="Dana");
root ++> alice ++> bob ++> charlie ++> dana;
finder = FindPerson(target="Bob");
root spawn finder;
print(f"Found: {finder.found}");
}
Output:
The walker stops at Bob and never visits Charlie or Dana.
Combining Report and Disengage#
Control Flow Patterns#
Pattern 1: Depth-First Search#
Pattern 2: Breadth-First Search#
walker BreadthFirst {
has visited: set = set();
has queue: list = [];
can start with `root entry {
self.queue = [root -->];
}
can traverse with Person entry {
if here.name not in self.visited {
self.visited.add(here.name);
print(f"Visiting {here.name}");
# Add neighbors to queue
neighbors = [here -->];
for n in neighbors {
if n.name not in self.visited {
self.queue.append(n);
}
}
}
# Visit next in queue
if self.queue {
visit self.queue.pop(0);
}
}
}
Pattern 3: Conditional Traversal#
Complete Example: Friend Recommendations#
node User {
has username: str;
has interests: list;
}
edge Friendship {
has since: int;
has strength: int = 5;
}
walker FriendRecommender {
has recommendations: list = [];
has visited: set = set();
can start with `root entry {
visit [-->];
}
can find_recommendations with User entry {
if here.username in self.visited {
return;
}
self.visited.add(here.username);
print(f"Finding recommendations for {here.username}...");
# Get friends of friends (2 hops)
friends_of_friends = [
here ->:Friendship:-> ->:Friendship:->
];
# Get direct friends to exclude
direct_friends = [here ->:Friendship:->];
for person in friends_of_friends {
# Don't recommend current friends or self
if person not in direct_friends and person != here {
if person.username not in self.recommendations {
self.recommendations.append(person.username);
}
}
}
disengage; # Found recommendations for first user
}
}
with entry {
alice = User(username="alice", interests=["coding", "hiking"]);
bob = User(username="bob", interests=["coding", "gaming"]);
charlie = User(username="charlie", interests=["hiking", "reading"]);
dana = User(username="dana", interests=["photography", "travel"]);
root ++> alice;
root ++> bob;
root ++> charlie;
root ++> dana;
alice +>:Friendship(since=2020, strength=8):+> bob;
bob +>:Friendship(since=2021, strength=6):+> charlie;
alice +>:Friendship(since=2019, strength=9):+> dana;
# Get recommendations for Alice
recommender = FriendRecommender();
alice spawn recommender;
print(f"Recommendations for Alice: {recommender.recommendations}");
}
graph TB
subgraph "Social Network"
Alice[User: Alice]
Bob[User: Bob]
Charlie[User: Charlie]
Dana[User: Dana]
end
Alice -->|Friendship<br/>since: 2020<br/>strength: 8| Bob
Bob -->|Friendship<br/>since: 2021<br/>strength: 6| Charlie
Alice -->|Friendship<br/>since: 2019<br/>strength: 9| Dana
subgraph "Recommendation Logic"
R1[Alice's friends:<br/>Bob, Dana]
R2[Bob's friends:<br/>Charlie]
R3[Dana's friends:<br/>none]
R4[Recommend:<br/>Charlie]
end
Alice -.-> R1
R1 -.-> R2
R1 -.-> R3
R2 -.-> R4
style R4 fill:#90EE90
Traversal Control Summary#
| Mechanism | Purpose | Stops Walker? | Returns Data? |
|---|---|---|---|
visit |
Navigate to nodes | No | No |
report |
Send data back | No | Yes (streams) |
disengage |
Stop immediately | Yes | No |
return |
Exit ability | No (just ability) | No |
Putting It All Together: The Four Concepts#
Let's see how all four concepts work together in a complete, real-world example:
# Concept 1: Spatial Object Model
node Task {
has title: str;
has status: str = "pending"; # pending, in_progress, complete
has priority: int = 1; # 1=low, 2=medium, 3=high
}
edge DependsOn {
# Task must wait for dependency to complete
}
# Concept 2: Mobile Computation (Walkers)
walker TaskAnalyzer {
has ready_tasks: list = [];
has blocked_tasks: list = [];
# Concept 3: Event-Driven Interaction (Abilities)
can start with `root entry {
visit [-->];
}
can analyze with Task entry {
if here.status != "pending" {
return; # Skip non-pending tasks
}
# Check if all dependencies are complete
dependencies = [here ->:DependsOn:->];
all_done = True;
for dep in dependencies {
if dep.status != "complete" {
all_done = False;
break;
}
}
# Concept 4: Traversal & Control Flow
if all_done {
self.ready_tasks.append(here.title);
} else {
self.blocked_tasks.append(here.title);
}
visit [-->]; # Continue to next tasks
}
}
walker MarkComplete {
has task_title: str;
can start with `root entry {
visit [-->];
}
can mark with Task entry {
if here.title == self.task_title {
here.status = "complete";
print(f"✓ Completed: {here.title}");
disengage;
}
visit [-->];
}
}
with entry {
# Build task graph
task1 = Task(title="Design database", priority=3);
task2 = Task(title="Implement API", priority=3);
task3 = Task(title="Create frontend", priority=2);
task4 = Task(title="Write tests", priority=2);
task5 = Task(title="Deploy", priority=3);
# Define dependencies
task2 +>:DependsOn:+> task1; # API needs database
task3 +>:DependsOn:+> task2; # Frontend needs API
task4 +>:DependsOn:+> task2; # Tests need API
task5 +>:DependsOn:+> task3; # Deploy needs frontend
task5 +>:DependsOn:+> task4; # Deploy needs tests
# Connect to root
root ++> task1;
root ++> task2;
root ++> task3;
root ++> task4;
root ++> task5;
# Analyze what's ready
analyzer = TaskAnalyzer();
root spawn analyzer;
print(f"\nReady to work on: {analyzer.ready_tasks}");
print(f"Blocked tasks: {analyzer.blocked_tasks}");
# Complete first task
root spawn MarkComplete(task_title="Design database");
# Analyze again
analyzer2 = TaskAnalyzer();
root spawn analyzer2;
print(f"\nAfter completion:");
print(f"Ready to work on: {analyzer2.ready_tasks}");
print(f"Blocked tasks: {analyzer2.blocked_tasks}");
}
Dependency Graph:
graph TB
T1[Design database<br/>Priority: 3]
T2[Implement API<br/>Priority: 3]
T3[Create frontend<br/>Priority: 2]
T4[Write tests<br/>Priority: 2]
T5[Deploy<br/>Priority: 3]
T2 -->|DependsOn| T1
T3 -->|DependsOn| T2
T4 -->|DependsOn| T2
T5 -->|DependsOn| T3
T5 -->|DependsOn| T4
style T1 fill:#90EE90
style T2 fill:#FFB6C6
style T3 fill:#FFB6C6
style T4 fill:#FFB6C6
style T5 fill:#FFB6C6
The Four Concepts: Summary Table#
| Concept | What It Is | Key Elements | What It Enables |
|---|---|---|---|
| 1. Spatial Object Model | How objects are structured using classes with spatial semantics | node (class), edge (class), ++>, [-->] |
First-class relationships, declarative queries, full OOP |
| 2. Mobile Computation | How computation moves using walker classes | walker (class), spawn, visit |
Autonomous traversal, data-centric computation |
| 3. Event-Driven Interaction | How things react via abilities (special methods) | Abilities, with entry, here, self |
Automatic dispatch, bidirectional polymorphism |
| 4. Traversal & Control | How you navigate & collect data | visit patterns, report, disengage |
Sophisticated navigation, streaming results |
Remember: Nodes, edges, and walkers are all full-featured classes. They support inheritance, methods, properties, and all OOP features—OSP adds spatial and traversal capabilities on top.
When to Use Object-Spatial Programming#
OSP shines in specific scenarios:
Perfect For:#
✓ Social networks - Users, friendships, followers ✓ Knowledge graphs - Concepts and their relationships ✓ Recommendation systems - Users, products, ratings ✓ Workflow systems - Tasks with dependencies ✓ Transportation networks - Cities, routes, connections ✓ Organization charts - People and reporting structures ✓ Dependency management - Packages, versions, requirements
Not Ideal For:#
✗ Simple CRUD applications ✗ Data with no significant relationships ✗ Performance-critical tight loops ✗ Simple list/table processing
The fundamental difference:
| Traditional OOP | Object-Spatial Programming |
|---|---|
| Objects are isolated | Objects (nodes) are spatially connected |
| Relationships are data in lists/dicts | Relationships are first-class objects (edges) |
| Computation is static | Computation is mobile (walkers) |
| Method calls are explicit | Abilities dispatch automatically |
| One-way interaction | Bidirectional interaction |
Conclusion: Just Four Things#
You've now learned the complete model of Object-Spatial Programming through four fundamental concepts:
- The Spatial Object Model - Structure your objects in space with nodes and edges (classes with spatial semantics)
- The Mobile Computation Model - Send walkers (classes) to travel through your object graph
- The Event-Driven Interaction Model - Define abilities (special methods) for automatic, bidirectional interaction
- The Traversal & Control Flow - Navigate with visit, collect with report, control with disengage
That's it. Four concepts that unlock an entirely new way of thinking about computation.
The fundamental insight about OSP:
Nodes, edges, and walkers are not new constructs—they're classes with all the power of OOP (methods, inheritance, polymorphism) plus spatial and traversal capabilities.
This means you keep all the programming knowledge you already have and add spatial semantics on top. When your problem domain involves complex relationships and interconnected data, Object-Spatial Programming provides a natural, powerful way to model and solve it.
The key insight:
"Don't bring data to computation. Send computation to where the data lives."
This simple shift in perspective—from static objects to spatial objects, from method calls to mobile computation—opens up new possibilities for how we structure programs. Welcome to Object-Spatial Programming.
Further Learning#
Want to dive deeper? Explore these resources:
- Jac Language Documentation - Complete language reference
- Example Projects - Real-world OSP applications
- Graph Algorithms in OSP - Classic algorithms reimagined
- Distributed OSP - Scaling walkers across machines
Ready to build something? Start with a simple social network, then work your way up to more complex graph-based applications. The four concepts you've learned here are all you need to get started.
Happy spatial programming! 🚀