Skip to content

Jac in a Flash#

This mini tutorial uses a single toy program to highlight the major pieces of the Jac language. We start with a small Python game and gradually evolve it into a fully data‑spatial Jac implementation. Each iteration introduces a new Jac feature while keeping the overall behaviour identical.

Step 0 – The Python version#

Our starting point is a regular Python program that implements a simple "guess the number" game. The player has several attempts to guess a randomly generated number.

"""A Number Guessing Game"""

import random


class Game:
    def __init__(self, attempts):
        self.attempts = attempts

    def play(self):
        raise NotImplementedError("Subclasses must implement this method.")


class GuessTheNumberGame(Game):
    def __init__(self, attempts=10):
        super().__init__(attempts)
        self.correct_number = random.randint(1, 10)

    def play(self):
        while self.attempts > 0:
            guess = input("Guess a number between 1 and 10: ")
            if guess.isdigit():
                if self.process_guess(int(guess)):
                    print("Congratulations! You guessed correctly.")
                    return  # Exit the game after a correct guess
            else:
                print("That's not a valid number! Try again.")

            self.attempts -= 1
            if self.attempts > 0:
                print(f"You have {self.attempts} attempts left.")

        print("Sorry, you didn't guess the number. Better luck next time!")

    def process_guess(self, guess):
        if guess > self.correct_number:
            print("Too high!")
        elif guess < self.correct_number:
            print("Too low!")
        else:
            return True
        return False


game = GuessTheNumberGame()
game.play()

Step 1 – A direct Jac translation#

guess_game1.jac mirrors the Python code almost line for line. Classes are declared with obj and methods with def. Statements end with a semicolon and the parent initializer is invoked via super.init. Program execution happens inside a with entry { ... } block, which replaces Python's if __name__ == "__main__": section. This step shows how familiar Python concepts map directly to Jac syntax.

"""A Number Guessing Game"""

import random;


obj Game {
    def init(attempts: int) {
        self.attempts = attempts;
        self.won = False;
    }

    def play() {
        raise NotImplementedError("Subclasses must implement this method.") ;
    }
}

obj GuessTheNumberGame (Game) {
    def init(attempts: int = 10) {
        super.init(attempts);
        self.correct_number = random.randint(1, 10);
    }

    def play() {
        while self.attempts > 0 {
            guess = input("Guess a number between 1 and 10: ");
            if guess.isdigit() {
                self.process_guess(int(guess));
                if self.won {
                    break;
                }
            } else {
                print("That's not a valid number! Try again.");
            }
        }
        if not self.won {
            print("Sorry, you didn't guess the number. Better luck next time!");
        }
    }

    def process_guess(guess: int) {
        if guess > self.correct_number {
            print("Too high!");
        } elif guess < self.correct_number {
            print("Too low!");
        } else {
            print("Congratulations! You guessed correctly.");
            self.won = True;
            return;
        }
        self.attempts -= 1;
        print(f"You have {self.attempts} attempts left.");
    }
}


with entry {
    game = GuessTheNumberGame();
    game.play();
}

Step 2 – Declaring fields with has#

The second version moves attribute definitions into the class body using the has keyword. Fields may specify types and default values directly on the declaration. Methods that take no parameters omit parentheses in their signature, making the object definition concise.

"""A Number Guessing Game"""

import random;


obj Game {
    has attempts: int, won: bool = False;

    def play {
        raise NotImplementedError("Subclasses must implement this method.");
    }
}


obj GuessTheNumberGame (Game) {
    has attempts: int = 10, correct_number: int = random.randint(1, 10);

    def play {
        while self.attempts > 0 {
            guess = input("Guess a number between 1 and 10: ");
            if guess.isdigit() {
                self.process_guess(int(guess));
                if self.won {
                    break;
                }
            } else {
                print("That's not a valid number! Try again.");
            }
        }
        if not self.won {
            print("Sorry, you didn't guess the number. Better luck next time!");
        }
    }

    def process_guess(guess: int) {
        if guess > self.correct_number {
            print("Too high!");
        } elif guess < self.correct_number {
            print("Too low!");
        } else {
            print("Congratulations! You guessed correctly.");
            self.won = True;
            return;
        }
        self.attempts -= 1;
        print(f"You have {self.attempts} attempts left.");
    }
}


# Run the game
with entry {
    game = GuessTheNumberGame();
    game.play();
}

Step 3 – Introducing the pipe operator#

guess_game3.jac demonstrates Jac's pipe forward operator (|>). It pipes the result of the expression on the left as arguments to the function on the right. Calls like print("hi") become "hi" |> print and expressions can be chained (guess |> int |> self.process_guess). The pipe is also used for object construction and method invocation (|> GuessTheNumberGame).

"""A Number Guessing Game"""

import random;


obj Game {
    has attempts: int, won: bool = False;

    def play {
        raise "Subclasses must implement this method." |> NotImplementedError ;
    }
}


obj GuessTheNumberGame (Game) {
    has attempts: int = 10, correct_number: int = (1, 10) |> random.randint;

    override def play {
        while self.attempts > 0 {
            guess = "Guess a number between 1 and 10: " |> input;
            if |> guess.isdigit {
                guess |> int |> self.process_guess;
            } else {
                "That's not a valid number! Try again." |> print;
            }
        }
        if not self.won {
            "Sorry, you didn't guess the number. Better luck next time!" |> print;
        }
    }

    def process_guess(guess: int) {
        if guess > self.correct_number {
            "Too high!" |> print;
            self.attempts -= 1;
        } elif guess < self.correct_number {
            "Too low!" |> print;
            self.attempts -= 1;
        } else {
            "Congratulations! You guessed correctly." |> print;
            self.won = True;
            self.attempts = 0;
            return;
        }
    f"You have {self.attempts} attempts left." |> print;
}

}


# Run the game
 with entry {
    game = |> GuessTheNumberGame;
    |> game.play;
}

Step 4 – Separating implementation with impl#

The fourth version splits object declarations from their implementations using impl. The object lists method signatures (def init;, override def play;), and the actual bodies are provided later in impl Class.method blocks. This separation keeps the interface clean and helps organise larger codebases.

"""A Number Guessing Game"""
import random;


obj Game {
    has attempts: int, won: bool = False;

    def play;
}


obj GuessTheNumberGame (Game) {
    has correct_number: int = (1, 10) |> random.randint;

    def init;
    override def play;
    def process_guess(guess: int);
}

# Run the game
 with entry {
    game = |> GuessTheNumberGame;
    |> game.play;
}
impl Game.play {
    raise "Subclasses must implement this method." |> NotImplementedError ;
}


impl GuessTheNumberGame.init {
    self.attempts = 10;
    self.correct_number = (1, 10) |> random.randint;
}


impl GuessTheNumberGame.play {
    while self.attempts > 0 {
        guess = "Guess a number between 1 and 10: " |> input;
        if |> guess.isdigit {
            guess |> int |> self.process_guess;
        } else {
            "That's not a valid number! Try again." |> print;
        }
    }
    if not self.won {
        "Sorry, you didn't guess the number. Better luck next time!" |> print;
    }
}


impl GuessTheNumberGame.process_guess (guess: int) {
    if guess > self.correct_number {
        "Too high!" |> print;
        self.attempts -= 1;
    } elif guess < self.correct_number {
        "Too low!" |> print;
        self.attempts -= 1;
    } else {
        "Congratulations! You guessed correctly." |> print;
        self.won = True;
        self.attempts = 0;
        return;
    }
    f"You have {self.attempts} attempts left." |> print;
}

Step 5 – Walking the graph#

Finally guess_game5.jac re‑imagines the game using Jac's data‑spatial architecture. A walker visits a chain of turn nodes created with ++> edges. The walker moves with visit [-->] and stops via disengage when the guess is correct. The game is launched by spawning the walker at root. This example shows how conventional logic can become graph traversal.

"""A Number Guessing Game"""

import random;

walker GuessGame {
    has correct_number: int = (1, 100) |> random.randint;

    can start_game with `root entry;
    def process_guess(guess: int);
}

node turn {
    can check with GuessGame entry;
}

# Run the game
 with entry {
    :> GuessGame spawn root;
}
impl turn.check {
    guess = "Guess a number between 1 and 10: " |> input;
    if |> guess.isdigit {
        guess |> int |> visitor.process_guess;
    } else {
        "That's not a valid number! Try again." |> print;
    }
    visit [-->];
}

impl GuessGame.start_game {
    end : `root | turn = here;
    for i = 0 to i < 10 by i += 1 {
        end ++> (end := turn());
    }
    visit [-->];
}


impl GuessGame.process_guess (guess: int) {
    if guess > self.correct_number {
        "Too high!" |> print;
    } elif guess < self.correct_number {
        "Too low!" |> print;
    } else {
        "Congratulations! You guessed correctly." |> print;
        disengage;
    }
}

Happy code deconstructing!