Skip to content

1. Introduction to Jac#


Welcome to Jac, a revolutionary programming language that transforms how we think about computation and data relationships. This chapter introduces you to Jac's core concepts and shows why it represents a fundamental shift in programming paradigms.

Jac introduces Object-Spatial Programming (OSP), where computation moves to data rather than data moving to computation, enabling naturally distributed and scale-agnostic applications.

What is Jac and Why It Exists#


Jac emerged from the need to better handle interconnected, graph-like data structures that are common in modern applications - social networks, knowledge graphs, distributed systems, and AI workflows. Traditional programming languages treat relationships as secondary concerns, but Jac makes them first-class citizens.

Traditional Approach to Modelling Relationships#

In traditional programming, relationships can be modelled with a number of structures such as functions, methods, classes, pointer references, or even databases.

Lets consider how we might model a simple social network with friends and mutual connections in Python. First we define a Person class. A person class may contain a list of references to other Person objects to represent friendships.

class Person:
    def __init__(self, name):
        self.name = name
        self.friends = []

    def add_friend(self, friend):
        self.friends.append(friend)
        friend.friends.append(self)

Since friendships are a two-way relationship, we need to ensure that when one person adds a friend, the other person also has that friendship established. This requires additional logic in our methods.

# Create people and relationships
alice = Person("Alice")
bob = Person("Bob")
charlie = Person("Charlie")

# Establish relationships
alice.add_friend(bob)
bob.add_friend(alice)  # This is redundant but necessary in Python
bob.add_friend(charlie)
charlie.add_friend(bob)  # Again, redundant but necessary

Modelling Relationships Naturally#

Lets see how we might define these relationships in Jac, a language designed to handle relationships naturally and concisely.

First lets define a Person node and a FriendsWith edge to represent the friendship relationship. We will discuss nodes and edges in detail later, but for now, think of nodes as entities with state and edges as typed relationships between those entities.

node Person {
    has name: str;
}

edge FriendsWith;

That single line of code defines a Person node with a name attribute and a FriendsWith edge type. Now we can create people and establish friendships in a much more natural way:

# Create people
alice = root ++> Person(name="Alice");
bob = root ++> Person(name="Bob");
charlie = root ++> Person(name="Charlie");

# Create relationships naturally
alice <+:FriendsWith:+> bob;
bob <+:FriendsWith:+> charlie;

The Jac version is not only more concise but also naturally handles the spatial relationships between entities.

Object-Spatial Programming Paradigm Overview#


Object-Spatial Programming (OSP) is built on three fundamental concepts:

  • Nodes: Stateful entities that hold data
  • Edges: Typed relationships between nodes
  • Walkers: Mobile computation that traverses the graph

Nodes: Data with Location#

Nodes are like enhanced objects that exist in a spatial graph:

node User {
    has username: str;
    has email: str;
    has created_at: str;
}

node Post {
    has title: str;
    has content: str;
    has likes: int = 0;
}

with entry {
    # Create nodes in the graph
    user = root ++> User(
        username="alice",
        email="alice@example.com",
        created_at="2024-01-01"
    );

    post = user ++> Post(
        title="Hello Jac!",
        content="My first post in Jac"
    );

    print(f"User {user[0].username} created post: {post[0].title}");
}

Edges: First-Class Relationships#

Edges represent typed connections between nodes:

node Person {
    has name: str;
    has age: int;
}

edge FamilyRelation {
    has relationship_type: str;
}

with entry {
    # Create family members
    parent = root ++> Person(name="John", age=45);
    child1 = root ++> Person(name="Alice", age=20);
    child2 = root ++> Person(name="Bob", age=18);

    # Create family relationships
    parent +>:FamilyRelation(relationship_type="parent"):+> child1;
    parent +>:FamilyRelation(relationship_type="parent"):+> child2;
    child1 +>:FamilyRelation(relationship_type="sibling"):+> child2;

    # Query relationships
    children = [parent[0]->:FamilyRelation:relationship_type=="parent":->(`?Person)];

    print(f"{parent[0].name} has {len(children)} children:");
    for child in children {
        print(f"  - {child.name} (age {child.age})");
    }
}

Walkers: Mobile Computation#

Walkers are computational entities that move through the graph:

node Person {
    has name: str;
    has visited: bool = False;
}

edge FriendsWith;

walker GreetFriends {
    can greet with Person entry {
        if not here.visited {
            here.visited = True;
            print(f"Hello, {here.name}!");

            # Visit all friends
            visit [->:FriendsWith:->];
        }
    }
}

with entry {
    # Create friend network
    alice = root ++> Person(name="Alice");
    bob = root ++> Person(name="Bob");
    charlie = root ++> Person(name="Charlie");

    # Connect friends
    alice +>:FriendsWith:+> bob +>:FriendsWith:+> charlie;
    alice +>:FriendsWith:+> charlie;  # Alice also friends with Charlie

    # Spawn walker to greet everyone
    alice[0] spawn GreetFriends();
}

Scale-Agnostic Programming Concept#


One of Jac's most powerful features is scale-agnostic programming - code written for one user automatically scales to multiple users and distributed systems.

The same Jac code works whether you have 1 user or 1 million users, running on a single machine or distributed across the globe.

Automatic Persistence#

Jac automatically persists data connected to the root node, so you don't need to manage databases or storage manually. This allows you to focus on relationships and computation rather than infrastructure.

The following example demonstrates a simple counter that persists automatically when deployed to a Jac server:

node Counter {
    has count: int = 0;

    def increment() -> None;
}

impl Counter.increment {
    self.count += 1;
    print(f"Counter is now: {self.count}");
}

with entry {
    # Get or create counter
    counters = [root-->(`?Counter)];
    if not counters {
        counter = root ++> Counter();
        print("Created new counter");
    }

    # Increment and save automatically
    counter[0].increment();
}

Multi-User Isolation#

Each user automatically gets their own isolated graph space:

node UserProfile {
    has username: str;
    has bio: str = "";
}

walker GetProfile {
    can get_user_info with entry {
        # 'root' automatically refers to current user's space
        profiles = [root-->(`?UserProfile)];
        if profiles {
            profile = profiles[0];
            print(f"Profile: {profile.username}");
            print(f"Bio: {profile.bio}");
        } else {
            print("No profile found");
        }
    }
}

walker CreateProfile {
    has username: str;
    has bio: str;

    can create with entry {
        # Each user gets their own isolated root
        profile = root ++> UserProfile(
            username=self.username,
            bio=self.bio
        );
        print(f"Created profile for {profile[0].username}");
    }
}

with entry {
    # This code works for any user automatically
    CreateProfile(username="alice", bio="Jac developer") spawn root;
    GetProfile() spawn root;
}

Comparison with Python and Traditional Languages#


While Jac maintains familiar syntax for Python developers, it introduces powerful new concepts:

Syntax Familiarity#

# Variables and functions work similarly
def calculate_average(numbers: list[float]) -> float {
    if len(numbers) == 0 {
        return 0.0;
    }
    return sum(numbers) / len(numbers);
}

with entry {
    scores = [85.5, 92.0, 78.5, 96.0, 88.5];
    avg = calculate_average(scores);
    print(f"Average score: {avg}");

    # Control flow is familiar
    if avg >= 90.0 {
        print("Excellent performance!");
    } elif avg >= 80.0 {
        print("Good performance!");
    } else {
        print("Needs improvement.");
    }
}

Key Differences#

Aspect Python Jac
Relationships Manual references First-class edges
Persistence External database Automatic
Multi-user Manual session management Built-in isolation
Distribution Complex setup Transparent
Graph Operations Manual traversal Spatial queries
Type System Optional hints Mandatory annotations

Simple Friend Network Example#


Let's build a complete friend network example that demonstrates Jac's core concepts.

Creating nodes#

First lets define our Person node and a FriendsWith edge to represent friendships:

node Person {
    has name: str;
    has age: int;
    has interests: list[str] = [];
}

edge FriendsWith {
    has since: str;
    has closeness: int;  # 1-10 scale
}

We can now create simple friends and establish friendships with metadata:

# Create friend network
alice = root ++> Person(
    name="Alice",
    age=25,
    interests=["coding", "music", "hiking"]
);

bob = root ++> Person(
    name="Bob",
    age=27,
    interests=["music", "sports", "cooking"]
);

charlie = root ++> Person(
    name="Charlie",
    age=24,
    interests=["coding", "gaming", "music"]
);

# Create friendships with metadata
alice +>:FriendsWith(since="2020-01-15", closeness=8):+> bob;
alice +>:FriendsWith(since="2021-06-10", closeness=9):+> charlie;
bob +>:FriendsWith(since="2020-12-03", closeness=7):+> charlie;

Creating the walker#

Next, lets create a walker to analyze the friend network and find common interests:

walker FindCommonInterests {
    has target_person: Person;
    has common_interests: list[str] = [];

    can find_common with Person entry {
        if here == self.target_person {
            return;  # Skip self
        }

        # Find shared interests
        shared = [];
        for interest in here.interests {
            if interest in self.target_person.interests {
                shared.append(interest);
            }
        }

        if shared {
            self.common_interests.extend(shared);
            print(f"{here.name} and {self.target_person.name} both like: {', '.join(shared)}");
        }
    }
}

To use the walker, you need to spawn it on a node of type Person—this node is provided as the first argument to the walker. As the walker traverses the graph, it maintains a list of common interests which is stored in the common_interests attribute and updates whenever it visits other Person nodes. The walker’s find_common ability is automatically triggered each time it encounters a Person node, where it compares interests with the target person and prints any shared interests it finds.

Bringing it all together#

Finally, we can use the walker to analyze Alice's friends and find common interests:

node Person {
    has name: str;
    has age: int;
    has interests: list[str] = [];
}

edge FriendsWith {
    has since: str;
    has closeness: int;  # 1-10 scale
}

walker FindCommonInterests {
    has target_person: Person;
    has common_interests: list[str] = [];

    can find_common with Person entry {
        if here == self.target_person {
            return;  # Skip self
        }

        # Find shared interests
        shared = [];
        for interest in here.interests {
            if interest in self.target_person.interests {
                shared.append(interest);
            }
        }

        if shared {
            self.common_interests.extend(shared);
            print(f"{here.name} and {self.target_person.name} both like: {', '.join(shared)}");
        }
    }
}

with entry {
    # Create friend network
    alice = root ++> Person(
        name="Alice",
        age=25,
        interests=["coding", "music", "hiking"]
    );

    bob = root ++> Person(
        name="Bob",
        age=27,
        interests=["music", "sports", "cooking"]
    );

    charlie = root ++> Person(
        name="Charlie",
        age=24,
        interests=["coding", "gaming", "music"]
    );

    # Create friendships with metadata
    alice +>:FriendsWith(since="2020-01-15", closeness=8):+> bob;
    alice +>:FriendsWith(since="2021-06-10", closeness=9):+> charlie;
    bob +>:FriendsWith(since="2020-12-03", closeness=7):+> charlie;

    print("=== Friend Network Analysis ===");

    # Find Alice's friends
    alice_friends = [alice[0]->:FriendsWith:->(`?Person)];
    print(f"Alice's friends: {[f.name for f in alice_friends]}");

    # Find common interests between Alice and her friends
    finder = FindCommonInterests(target_person=alice[0]);
    for friend in alice_friends {
        friend spawn finder;
    }

    # Find close friendships (closeness >= 8)
    close_friendships = [root-->->:FriendsWith:closeness >= 8:->];
    print(f"Close friendships ({len(close_friendships)} found):");
}


This example demonstrates how Jac's Object-Spatial Programming model allows you to express complex relationships and computations in a natural, intuitive way. The walker traverses the graph, finding common interests and printing them out, all while maintaining the relationships defined by edges.

Key Takeaways#


Core Concepts:

  • Object-Spatial Programming (OSP) moves computation to data through nodes, edges, and walkers
  • Nodes are enhanced objects that exist in spatial relationships
  • Edges represent first-class, typed relationships between nodes
  • Walkers are mobile computational entities that traverse graphs

Key Advantages:

  • Scale-agnostic programming: Code automatically works from single-user to distributed systems
  • Automatic persistence: Data connected to root persists without manual database management
  • Natural relationships: Graph traversal is intuitive and expressive
  • Multi-user isolation: Built-in user separation without complex session management
  • Python familiarity: Familiar syntax with powerful new capabilities

Start thinking about problems in your domain that involve relationships: - Social networks and user connections - Knowledge graphs and information relationships - Workflow systems and process connections - Data pipelines and transformation chains

Note

Remember: Jac shines when your problem naturally involves connected data!


Ready to start your Object-Spatial Programming journey? Let's get your environment set up and build your first Jac application!