Formulas and Reactivity
I drew a lot of inspiration from spreadsheets. Seriously. Think about what Excel does — you put a formula in a cell, it references other cells, and when those cells change, the formula updates automatically. No callbacks, no event listeners, no manual invalidation. It just works.
That's what I wanted for Adama. You define a formula, the runtime tracks its dependencies, and when any dependency changes, the formula's value updates and connected clients get the new result. No synchronization code required.
Defining Formulas
The formula keyword creates a computed value:
public int x = 0;
public int y = 0;
public formula distance = Math.sqrt(x * x + y * y);
When x or y changes, distance automatically reflects the new value. Clients subscribed to the document receive the updated distance without any explicit recalculation code.
Basic Syntax
// Simple arithmetic
public int count = 0;
public formula doubled = count * 2;
public formula tripled = count * 3;
// String concatenation
public string firstName = "Alice";
public string lastName = "Smith";
public formula fullName = firstName + " " + lastName;
// Using functions
public double x = 3.0;
public double y = 4.0;
public formula hypotenuse = Math.sqrt(x * x + y * y);
Formulas Can Reference Other Formulas
Formulas can build on other formulas, creating chains of computed values:
public int price = 100;
public double taxRate = 0.08;
public formula tax = price * taxRate;
public formula total = price + tax;
public formula displayTotal = "$" + total;
When price changes, all three formulas update in the correct order. You don't manage the ordering — the runtime does.
Formula Rules
Formulas have constraints. These aren't arbitrary — they exist to make the reactive system actually work.
Rule 1: Formulas Must Be Pure
The right-hand side of a formula must be pure — it cannot mutate the document in any way. This restricts formulas to:
- Mathematical expressions
- Function calls (not procedure calls)
- Field references
- Literal values
int counter = 0;
// VALID: pure computation
public formula doubled = counter * 2;
// INVALID: procedures can mutate state
procedure increment() {
counter++;
}
public formula bad = increment(); // ERROR: Cannot call procedures in formulas
This is the tradeoff I mentioned in the fundamentals chapter — the reason functions and procedures are separate concepts. Functions are pure, so they're safe to call from formulas. Procedures can mutate state, so they're not.
Rule 2: Only Reference Previously Defined State
Formulas can only reference state (fields or other formulas) that was defined before the formula in the source file. This prevents circular dependencies:
public int a = 1;
public formula b = a + 1; // OK: 'a' defined before 'b'
public formula c = a + b; // OK: 'a' and 'b' defined before 'c'
The following would cause an error:
public formula x = y + 1; // ERROR: 'y' not yet defined
public int y = 5;
Rule 3: No Circular Dependencies
Because of Rule 2, circular dependencies are impossible by construction:
// This cannot happen in Adama because one formula
// must be defined before the other
public formula a = b + 1; // 'b' not defined yet - ERROR
public formula b = a + 1; // Even if we tried
I'm pretty happy about this. Some reactive systems try to detect cycles at runtime. I just made them structurally impossible. One less class of bugs to worry about.
Lazy Evaluation
Formulas in Adama are 100% lazy:
- No computation until accessed: A formula's expression is not evaluated until its value is needed
- Results are cached: Once computed, the result is stored and reused
- Invalidation on change: When dependencies change, the cached result is discarded
- Recompute on next access: The formula recomputes only when accessed again after invalidation
public int a = 1;
public int b = 2;
public int c = 3;
// These formulas only compute when a client requests them
public formula sum = a + b + c;
public formula product = a * b * c;
public formula computed = Math.sqrt(a * a + b * b) + c * c;
If no client ever accesses computed, its computation never runs. If a client accesses sum multiple times without a, b, or c changing, the cached result is returned instantly. This makes formulas essentially free when they're not being used — which matters when you have documents with hundreds of formulas but only a few viewers looking at a subset of them.
Formulas with Tables
Formulas get really interesting when combined with table queries. The iterate keyword converts a table into a list that can be filtered, transformed, and aggregated — and the whole thing is reactive.
Aggregation Formulas
record Item {
public string name;
public int quantity;
public double price;
}
table<Item> items;
// Count all items
public formula itemCount = (iterate items).size();
// Sum a field using field selector
public formula totalQuantity = (iterate items).quantity.sum();
// Using Statistics function
public formula totalQuantity2 = Statistics.sum((iterate items).quantity);
// Average price
public formula averagePrice = (iterate items).price.average();
Note: Aggregation methods like
.sum()and.average()returnmaybe<T>because the list might be empty. Use.getOrDefaultTo()to provide a fallback value. Empty list, no average — makes sense if you think about it.
Filtering Formulas
record Task {
public string title;
public string status;
public int priority;
public principal assignee;
}
table<Task> tasks;
// Filter by status
public formula activeTasks = iterate tasks where status == "active";
public formula completedTasks = iterate tasks where status == "completed";
public formula pendingTasks = iterate tasks where status == "pending";
// Filter by priority
public formula highPriority = iterate tasks where priority > 5;
// Combine filters
public formula urgentActive = iterate tasks where status == "active" && priority >= 8;
// Count filtered results
public formula activeCount = (iterate tasks where status == "active").size();
Ordering and Limiting
record Task {
public int id;
public string title;
public string status;
public int priority;
}
table<Task> tasks;
// Order by priority (descending)
public formula topTasks = iterate tasks order by priority desc;
// Limit results
public formula top5 = iterate tasks order by priority desc limit 5;
// Combine with filtering
public formula top3Active = iterate tasks where status == "active"
order by priority desc limit 3;
Formulas in Records
Records can define their own formulas that operate on the record's fields:
record Order {
public int quantity;
public double unitPrice;
public double taxRate = 0.08;
// Computed fields within the record
public formula subtotal = quantity * unitPrice;
public formula tax = subtotal * taxRate;
public formula total = subtotal + tax;
}
table<Order> orders;
// Document-level formula aggregating record formulas
public formula grandTotal = (iterate orders).total.sum();
Each order instance maintains its own computed subtotal, tax, and total. When an order's quantity or unitPrice changes, that order's formulas update automatically. And then the document-level grandTotal updates too. Reactive all the way down.
Nested Record Formulas
record Player {
public string name;
public int wins = 0;
public int losses = 0;
public int draws = 0;
public formula gamesPlayed = wins + losses + draws;
public formula winRate = wins / (wins + losses + draws);
public formula points = wins * 3 + draws;
}
table<Player> players;
// Leaderboard sorted by points
public formula leaderboard = iterate players order by points desc;
// Total games across all players
public formula totalGames = (iterate players).gamesPlayed.sum();
Bubbles (Per-Viewer Formulas)
Formulas produce the same value for all viewers. But what if different people need to see different things? That's where bubbles come in. A bubble is a formula that incorporates @who — the identity of the currently viewing user — so each connected client sees personalized data.
The @who Constant
Inside a bubble, @who represents the identity (principal) of the currently viewing user:
record Card {
public int value;
public principal owner;
}
table<Card> deck;
// Each viewer sees only their own cards
bubble myCards = iterate deck where owner == @who;
When Alice connects, her myCards bubble shows cards where owner == Alice. When Bob connects, his myCards shows cards where owner == Bob. Same bubble definition, different results per viewer. This is the core idea behind Adama's privacy model — the document is one thing, but each viewer sees their own projection of it.
Common Bubble Patterns
record Message {
public string content;
public principal sender;
public principal recipient;
}
table<Message> messages;
// Messages sent TO the current viewer
bubble inbox = iterate messages where recipient == @who;
// Messages sent BY the current viewer
bubble sent = iterate messages where sender == @who;
// All messages involving the current viewer
bubble myMessages = iterate messages
where sender == @who || recipient == @who;
record GamePiece {
public int x;
public int y;
public principal controller;
public bool hidden;
}
table<GamePiece> pieces;
// Show your own pieces (including hidden) plus others' visible pieces
bubble visiblePieces = iterate pieces
where controller == @who || !hidden;
WARNING: Bubbles are ephemeral — they exist only in the context of a viewer connection. You cannot use bubble fields within document logic (channels, procedures, state machines). Bubbles are purely for sending customized data to connected viewers. I know this trips people up, but there's no clean way around it.
record Item {
public string name;
public principal owner;
}
table<Item> items;
message Empty {}
bubble myData = iterate items where owner == @who;
channel doSomething(Empty msg) {
// ERROR: Cannot reference 'myData' here!
// Bubbles don't exist in channel/procedure context
}
Bubbles Inside Records
Bubbles can be declared inside records, not just at the document level. This allows per-viewer computed values scoped to a specific record:
record TeamMember {
public string name;
public principal account;
}
function has_notifications_for(principal who_p) -> bool {
return true;
}
record Team {
public int capacity;
table<TeamMember> _members;
public formula count_members = _members.size();
bubble over_capacity = count_members > capacity;
bubble has_notifications = has_notifications_for(@who);
}
Policy-Gated Bubbles
Bubbles can be restricted to viewers who satisfy a policy using the bubble<policy_name> syntax:
record Person {
public principal account;
public bool is_admin;
public bool is_staff;
public string name;
}
table<Person> _people;
formula compute_admin_view = iterate _people;
policy is_admin {
if ((iterate _people where account == @who)[0] as person) {
return person.is_admin;
}
return false;
}
// Only admins see this data
bubble<is_admin> all_people = iterate _people;
bubble<is_admin> admin_dashboard = compute_admin_view;
// Staff and above
policy is_staff {
if ((iterate _people where account == @who)[0] as person) {
return person.is_staff;
}
return false;
}
bubble<is_staff> staff_view = iterate _people where is_staff;
This is more declarative than checking permissions within the bubble's expression, and it's what I'd recommend for any production application doing role-based data access.
Viewer State
Here's where things get really interesting. Viewers can send additional state to the document that influences their bubbles. This enables client-side features like filtering, sorting, and pagination — all reactive, all server-driven.
Declaring Viewer State
Use the view keyword to declare fields that each viewer can set:
// Each viewer has their own scroll position
view int scroll_x;
view int scroll_y;
// Each viewer has their own filter settings
view string categoryFilter;
view bool showCompleted;
Accessing Viewer State with @viewer
Inside a bubble, @viewer provides access to the viewer's state:
view string category;
record Product {
public string name;
public string category;
public double price;
}
table<Product> products;
// Filter products based on viewer's selected category
bubble filteredProducts = iterate products
where @viewer.category == "" || category == @viewer.category;
Pagination Example
view int pageNumber;
view int pageSize;
record Post {
public string title;
public string content;
public datetime createdAt;
}
table<Post> posts;
// Paginated results based on viewer's page settings
bubble paginatedPosts = iterate posts
order by createdAt desc
offset @viewer.pageNumber * @viewer.pageSize
limit @viewer.pageSize;
Dynamic Sorting
view string sortField;
record Person {
public string name;
public int age;
public datetime joined;
}
table<Person> people;
// Sort dynamically based on viewer preference
// order_dyn accepts a string like "age" or "-age" (descending)
bubble sortedPeople = iterate people order_dyn @viewer.sortField;
The order_dyn clause accepts a dynamic sort string:
"age"- sort by age ascending"-age"- sort by age descending"age,name"- sort by age, then by name
Combining Viewer State with @who
view int limit;
record Task {
public string title;
public principal owner;
public int priority;
}
table<Task> tasks;
// Show current viewer's tasks, limited to their specified count
bubble myTasks = iterate tasks
where owner == @who
order by priority desc
limit @viewer.limit;
Privacy Modifiers with Formulas
Formulas respect the same privacy modifiers as regular fields:
public int publicScore = 100;
private int secretMultiplier = 3;
// This formula is public - visible to all viewers
public formula displayScore = publicScore;
// This formula is private - never sent to clients
private formula internalScore = publicScore * secretMultiplier;
// The privacy modifier controls who can see the formula result
Use Cases
Real-Time Dashboard
record Sale {
public double amount;
public datetime timestamp;
public string region;
}
table<Sale> sales;
// Dashboard metrics that update in real-time
public formula totalSales = (iterate sales).amount.sum();
public formula saleCount = (iterate sales).size();
public formula averageSale = (iterate sales).amount.average();
// Regional breakdown
public formula northSales = (iterate sales where region == "north").amount.sum();
public formula southSales = (iterate sales where region == "south").amount.sum();
Game State
record Player {
public string name;
public int score;
public bool alive;
}
table<Player> players;
public formula activePlayers = iterate players where alive;
public formula playerCount = (iterate players where alive).size();
public formula leaderboard = iterate players order by score desc limit 10;
public formula winner = (iterate players order by score desc limit 1)[0];
Collaborative Document
record Edit {
public string content;
public principal author;
public datetime timestamp;
}
table<Edit> edits;
// Latest version
public formula currentContent = (iterate edits order by timestamp desc limit 1)[0].content;
// Per-viewer: show their own edit history
bubble myEdits = iterate edits where author == @who order by timestamp desc;