Interactive Tutorial
Learn Tova step by step. Each lesson builds on the previous one, taking you from your first variable to a full-stack application.
Lesson 1: Variables and Printing
Tova variables are immutable by default. To make a variable mutable, use var.
// Immutable — cannot be reassigned
name = "Alice"
age = 30
pi = 3.14159
// Print with string interpolation
print("Hello, {name}! You are {age} years old.")
// Mutable — can be reassigned
var count = 0
count += 1
count += 1
print("Count: {count}") // Count: 2Try it: Change name to your own name and run again.
Key Takeaways
- No keyword needed for immutable variables:
x = 5 - Use
varfor mutable variables:var x = 5 - String interpolation uses
{expr}inside double quotes - Single quotes
'...'disable interpolation
Lesson 2: Functions
Functions are declared with fn. The last expression is automatically returned.
fn add(a, b) {
a + b
}
fn greet(name, greeting = "Hello") {
"{greeting}, {name}!"
}
print(add(3, 4)) // 7
print(greet("Alice")) // Hello, Alice!
print(greet("Bob", "Hey")) // Hey, Bob!Lambdas
Anonymous functions come in two flavors:
// fn lambda
double = fn(x) x * 2
// Arrow lambda
triple = x => x * 3
// Multi-line lambda
process = fn(x) {
cleaned = x.trim()
cleaned.upper()
}
print(double(5)) // 10
print(triple(5)) // 15
print(process(" hi ")) // HITry it: Write a function fn square(n) that returns n * n.
Key Takeaways
fn name(params) { body }declares a function- The last expression is the return value (no
returnneeded) fn(x) exprandx => exprare anonymous function forms- Parameters can have defaults:
greeting = "Hello"
Lesson 3: Type Annotations
Tova has optional type annotations. Add them when they help readability or when you want the compiler to catch errors.
// Type annotations on variables
x: Int = 42
name: String = "Alice"
scores: [Int] = [90, 85, 92]
// Type annotations on functions
fn divide(a: Float, b: Float) -> Result<Float, String> {
if b == 0 {
Err("Division by zero")
} else {
Ok(a / b)
}
}
// Custom types
type User {
id: Int
name: String
email: String
}
alice = User(1, "Alice", "alice@example.com")
print(alice.name) // Alice
print(alice.email) // alice@example.comTry it: Define a type Book { title: String, author: String, pages: Int } and create an instance.
Key Takeaways
- Variables:
name: Type = value - Function params:
fn f(x: Int) -> String { ... } - Custom types:
type Name { field: Type } - Types are optional — Tova infers what it can
Lesson 4: Control Flow
If / Elif / Else
score = 85
if score >= 90 {
print("A")
} elif score >= 80 {
print("B")
} elif score >= 70 {
print("C")
} else {
print("F")
}
// if as expression
grade = if score >= 90 { "A" } elif score >= 80 { "B" } else { "C" }
print("Grade: {grade}")For Loops
fruits = ["apple", "banana", "cherry"]
for fruit in fruits {
print("I like {fruit}")
}
// With index
for i, fruit in fruits {
print("{i}: {fruit}")
}
// Range
for i in range(5) {
print(i) // 0, 1, 2, 3, 4
}Guard Clauses
fn process(value) {
guard value != nil else {
return Err("Value is nil")
}
guard value > 0 else {
return Err("Value must be positive")
}
Ok(value * 2)
}
print(process(5)) // Ok(10)
print(process(-1)) // Err(Value must be positive)
print(process(nil)) // Err(Value is nil)Try it: Write a function that takes a list of numbers and prints only the positive ones.
Key Takeaways
- Use
elif, neverelse if ifis an expression — it returns a valuefor x in items {}— no parentheses neededfor i, x in items {}gives you the indexguardflattens nested conditionals
Lesson 5: Pattern Matching
Pattern matching is one of Tova's most powerful features.
// Basic match
fn describe(x) {
match x {
0 => "zero"
1 => "one"
_ => "something else"
}
}
// Range patterns
fn grade(score) {
match score {
90..=100 => "A"
80..90 => "B"
70..80 => "C"
_ => "F"
}
}
// With guards
fn classify(n) {
match n {
n if n < 0 => "negative"
0 => "zero"
n if n % 2 == 0 => "positive even"
_ => "positive odd"
}
}
print(describe(0)) // zero
print(grade(85)) // B
print(classify(-3)) // negative
print(classify(4)) // positive evenMatching on ADTs
type Shape {
Circle(radius: Float)
Rectangle(width: Float, height: Float)
}
fn area(shape) {
match shape {
Circle(r) => 3.14159 * r * r
Rectangle(w, h) => w * h
}
}
print(area(Circle(5.0))) // 78.53975
print(area(Rectangle(4.0, 6.0))) // 24.0Try it: Add a Triangle(base: Float, height: Float) variant to Shape and handle it in area.
Key Takeaways
match value { pattern => result }is an expression_is the wildcard catch-all pattern- Range patterns:
0..10(exclusive),0..=10(inclusive) - Guards:
n if n > 0 => ... - ADT variants destructure naturally
Lesson 6: Collections and Pipes
List Comprehensions
squares = [x * x for x in range(10)]
print(squares) // [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
evens = [x for x in range(20) if x % 2 == 0]
print(evens) // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]Slicing
items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(items[2:5]) // [2, 3, 4]
print(items[:3]) // [0, 1, 2]
print(items[-3:]) // [7, 8, 9]
print(items[::2]) // [0, 2, 4, 6, 8]
print(items[::-1]) // [9, 8, 7, ..., 0]Pipe Operator
The |> operator passes the left-hand value as the first argument to the right-hand function:
// Without pipes — nested, hard to read
result = sorted(filter(map(numbers, fn(x) x * 2), fn(x) x > 0))
// With pipes — reads top to bottom
result = numbers
|> map(fn(x) x * 2)
|> filter(fn(x) x > 0)
|> sorted()
// Real-world example
active_emails = users
|> filter(fn(u) u.active)
|> map(fn(u) u.email)
|> sorted()
|> join(", ")Try it: Use pipes to take a list of numbers, filter out negatives, double each one, and sum the result.
Key Takeaways
[expr for x in items if condition]— list comprehensionitems[start:end:step]— Python-style slicingx |> f()is equivalent tof(x)- Chain pipes for readable data transformations
Lesson 7: Error Handling
Tova uses Result and Option types instead of exceptions.
Result
fn parse_age(input: String) -> Result<Int, String> {
n = to_int(input)
if n == nil {
Err("Not a number: {input}")
} elif n < 0 {
Err("Age cannot be negative")
} elif n > 150 {
Err("Age seems unrealistic")
} else {
Ok(n)
}
}
// Handle with match
match parse_age("25") {
Ok(age) => print("Your age is {age}")
Err(msg) => print("Error: {msg}")
}
// Chain with methods
display = parse_age("25")
.map(fn(age) "Age: {age}")
.unwrapOr("Invalid input")
print(display) // Age: 25Error Propagation with ?
fn process(input: String) -> Result<String, String> {
age = parse_age(input)? // returns Err early if it fails
category = categorize(age)?
Ok("You are {category}")
}Option
fn find_user(users, name) -> Option<User> {
for user in users {
if user.name == name {
return Some(user)
}
}
None
}
match find_user(users, "Alice") {
Some(user) => print("Found: {user.email}")
None => print("Not found")
}
// Or use unwrapOr for a default
email = find_user(users, "Alice")
.map(fn(u) u.email)
.unwrapOr("unknown@example.com")Try it: Write a function fn safe_divide(a, b) -> Result<Float, String> that returns Err for division by zero.
Key Takeaways
Result<T, E>=Ok(value)orErr(error)Option<T>=Some(value)orNone?propagates errors:risky_call()?.map(),.flatMap(),.unwrapOr()for chaining- No
throw— errors are values
Lesson 8: The Full-Stack Model
Tova's defining feature: write server and browser code in one file.
shared {
// Types available to both server and browser
type Message {
id: Int
text: String
author: String
}
}
server {
// Server-side: HTTP routes, database
db { path: "./chat.db" }
model MessageModel {}
fn get_messages() -> [Message] {
MessageModel.all()
}
fn send_message(text: String, author: String) -> Message {
MessageModel.create({ text, author })
}
}
browser {
// Browser-side: reactive UI
state messages: [Message] = []
state new_text = ""
state username = "Anonymous"
effect {
messages = server.get_messages()
}
component App {
<div class="chat">
<h1>Chat</h1>
<div class="messages">
for msg in messages {
<div class="message">
<strong>{msg.author}</strong>: {msg.text}
</div>
}
</div>
<div class="input">
<input bind:value={new_text} placeholder="Type a message..." />
<button on:click={fn() {
server.send_message(new_text, username)
new_text = ""
messages = server.get_messages()
}}>Send</button>
</div>
</div>
}
}How It Compiles
shared {}becomeschat.shared.js— imported by both sidesserver {}becomeschat.server.js— runs on Bun withBun.serve()browser {}becomeschat.client.js— embedded inindex.htmlwith the reactive runtimeserver.get_messages()in browser code compiles to afetch()call to an auto-generated RPC endpoint
Reactive Primitives
| Primitive | Purpose |
|---|---|
state x = value | Reactive signal — UI updates when it changes |
computed y = expr | Derived value — auto-recomputes when dependencies change |
effect { ... } | Side effect — re-runs when its dependencies change |
component Name { jsx } | UI component with optional props |
JSX in Tova
component TodoItem(todo, on_toggle) {
<li class={if todo.done { "done" } else { "" }}>
<input
type="checkbox"
checked={todo.done}
on:change={fn() on_toggle(todo.id)}
/>
<span>{todo.title}</span>
</li>
}Key Takeaways
shared {}— types and validation for both sidesserver {}— routes, database, business logic (runs on Bun)browser {}— reactive UI with JSX (runs in browser)server.fn_name()from browser code becomes an RPC call automaticallystate,computed,effectare the reactive primitives- JSX uses Tova control flow (
if,for) not JS expressions
Lesson 9: Stores and Components
Stores
Group related state into a store:
browser {
store Counter {
state count = 0
computed doubled = count * 2
computed is_even = count % 2 == 0
fn increment() {
count += 1
}
fn decrement() {
count -= 1
}
fn reset() {
count = 0
}
}
}Component Composition
component Button(label, on_click, variant = "primary") {
<button
class="btn btn-{variant}"
on:click={on_click}
>
{label}
</button>
}
component Counter {
state count = 0
<div>
<p>Count: {count}</p>
<Button label="+" on_click={fn() count += 1} />
<Button label="-" on_click={fn() count -= 1} />
<Button label="Reset" on_click={fn() count = 0} variant="secondary" />
</div>
}Conditional and List Rendering
component UserList(users, loading) {
if loading {
<p>Loading...</p>
} elif len(users) == 0 {
<p>No users found.</p>
} else {
<ul>
for user in users key={user.id} {
<li>
<strong>{user.name}</strong> — {user.email}
</li>
}
</ul>
}
}Key Takeaways
storegroups related state, computed values, and functions- Components accept props as function parameters
- Props can have defaults:
variant = "primary" - Use
if/elif/elseandfordirectly in JSX key={expr}onforloops optimizes list rendering
Lesson 10: Server Routes and Database
Declaring Routes
server {
db { path: "./app.db" }
model User {}
model Post {}
// Route declarations
route GET "/api/users" => list_users
route POST "/api/users" => create_user
route GET "/api/users/:id" => get_user
route DELETE "/api/users/:id" => delete_user
fn list_users() {
User.all()
}
fn create_user(req) {
User.create(req.body)
}
fn get_user(id: Int) {
User.find(id)
}
fn delete_user(id: Int) {
User.delete(id)
}
// Route groups
routes "/api/v1" {
route GET "/posts" => fn() Post.all()
route POST "/posts" with auth => fn(req) Post.create(req.body)
}
// Middleware
middleware fn auth(req, next) {
token = req.headers["authorization"]
if token == nil {
return respond(401, { error: "Unauthorized" })
}
next(req)
}
}Running Your App
# Create a new project
tova new my-app
cd my-app
# Start development server (hot reload)
tova dev
# Build for production
tova build --production
# Run tests
tova testKey Takeaways
route METHOD "/path" => handlerdeclares HTTP routesroute ... with guard => handleradds middlewareroutes "/prefix" { ... }groups routes under a pathdb { path: "..." }configures the databasemodel Name {}creates a database modeltova devfor development,tova buildfor production
Next Steps
You now know the fundamentals of Tova. Here is where to go next:
- Variables — deep dive into bindings and destructuring
- Functions — default params, destructuring params, closures
- Types — ADTs, generics, derive macros
- Pattern Matching — all pattern forms
- Pipes — pipe operator and pipeline patterns
- Error Handling — Result, Option, and
?operator - Full-Stack Architecture — the server/browser/shared model
- Reactivity — signals, computed, effects in depth
- Standard Library — all built-in functions and modules
- Examples — complete example applications