Tutorial¶
This tutorial walks you through Prove's core concepts step by step. By the end, you'll understand intent-first programming and why it's different from other languages.
Prerequisites¶
- Python 3.11+
- gcc or clang
- A text editor
Install Prove:
Your First Function¶
Every Prove function declares its intent before implementation. The verb tells the compiler (and readers) what the function does:
Let's break this down:
| Part | Meaning |
|---|---|
transforms | This is a pure function — no side effects |
double(n Integer) Integer | Takes an Integer, returns an Integer |
ensures result == n * 2 | Contract: the result must equal n times 2 |
from | Everything after this is the implementation |
n * 2 | The actual computation |
Try running it:
prove new hello
cd hello
# Edit src/main.prv to add the double function, then:
prove build
./build/hello
Verbs: Declaring Intent¶
Prove uses verbs instead of generic function or fn. Each verb has a specific meaning:
Pure Verbs (No Side Effects)¶
transforms add(a Integer, b Integer) Integer
from
a + b
validates is_positive(n Integer)
from
n > 0
reads first(items List<Integer>) Option<Integer>
from
len(items) > 0 => Some(items[0])
_ => None
creates builder() StringBuilder
from
allocate()
| Verb | Purpose |
|---|---|
transforms | Convert data from one form to another |
validates | Return true/false (implicitly Boolean) |
reads | Extract or query without modification |
creates | Construct a new value |
IO Verbs (Side Effects)¶
inputs read_file(path String) String!
from
file(path)
outputs write_file(path String, content String)!
from
file(path, content)
| Verb | Purpose |
|---|---|
inputs | Read from external world |
outputs | Write to external world |
The compiler enforces these distinctions. A transforms function cannot call inputs or outputs — the verb itself guarantees purity.
Types and Refinements¶
Types in Prove carry constraints, not just shapes:
# A port number — must be between 1 and 65535
type Port is Integer:[16 Unsigned] where 1 .. 65535
# An email — must match a regex pattern
type Email is String where r"^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$"
# A non-empty list — length must be greater than 0
type NonEmpty<Value> is List<Value> where len > 0
With NonEmpty, you never need to check if a list is empty — the type guarantees it:
Pattern Matching¶
Prove has no if/else. All branching uses match:
The compiler enforces exhaustiveness — if you add a new variant to Shape, it will error until you handle it everywhere.
Why No if?¶
- Types replace booleans — model your domain with types, not conditions
- Exhaustiveness is enforced — no forgotten edge cases
- One construct is simpler —
matchhandles all branching
Contracts: Proving Correctness¶
Contracts declare what a function guarantees. The compiler enforces them:
transforms apply_discount(price Price, discount Discount) Price
requires discount >= 0
requires price >= 0
ensures result >= 0
ensures result <= price
from
match discount
None => price
FlatOff(amount) => max(0, price - amount)
PercentOff(rate) => price * (1 - rate)
requires— what must be true before the function is calledensures— what is guaranteed after the function returns
Error Handling¶
Errors are values, not exceptions:
The ! marks fallibility — it propagates errors up the call chain. Only IO verbs (inputs, outputs) can use !.
For pure functions that need to represent failure, use Result:
transforms divide(a Decimal, b Decimal) Result<Decimal, String>
from
b == 0 => Err("division by zero")
_ => Ok(a / b)
No Null¶
Prove has no null. Use Option<Value> instead:
transforms find_user(id Integer) Option<User>
from
match query(db, "SELECT * FROM users WHERE id = {id}")!
[] => None
[user] => Some(user)
_ => None # Should never happen
Iteration: Functional Style¶
No loops. Use map, filter, reduce:
# Get names of all active users
names as List<String> = map(users, |u| u.name)
# Filter to active users only
active as List<User> = filter(users, |u| u.active)
# Calculate total price
total as Decimal = reduce(items, 0, |acc, item| acc + item.price)
# Chain operations with pipe
result as List<String> = users
|> filter(|u| u.active)
|> map(|u| u.email)
Async: Structured Concurrency¶
Three async verbs for different patterns:
# Fire and forget — caller doesn't wait
detached log(message String)
from
console(message)
# Spawn and await — caller waits for result
attached fetch(url String) String
from
request(url)&
# Event dispatcher — cooperative loop
listens handler(workers List<Attached>)
event_type Command
from
Process(data) => handle(data)&
Exit => Unit
The & marker dispatches to a coroutine. No threads, no data races.
What's Next?¶
- Read Syntax Reference for the complete grammar
- Explore the Standard Library modules
- See the Inventory Example for a full application
- Check the Roadmap for upcoming features