This tutorial walks you through building a complete todo-list program in Geno.
Along the way you will use algebraic data types, pattern matching, Result error
handling, list operations, pipelines, and example clauses. By the end you will
have a working program you can type-check, run, and extend into an interactive
app.
What We Are Building
A small todo manager that can:
- Create new todo items
- Mark a todo as done
- Filter todos by status
- Count todos by status
- Handle errors when an operation is invalid
Project Setup
Create a directory and two files:
todo/
Todo.geno
Main.geno
Todo.geno holds our types and core logic. Main.geno imports it and ties
everything together in a main function.
Defining Types
Geno models data with algebraic data types (ADTs). We need a Status type that
is either Pending or Done, and a Todo type that pairs a title with a
status.
Add the following to Todo.geno:
type Status = Pending | Done
type Todo = Todo(title: String, status: Status)
Pending and Done are nullary constructors -- they carry no data. Todo is a
constructor with two named fields. You create values the same way you declare
them:
let buy_milk: Todo = Todo("Buy milk", Pending)
let walk_dog: Todo = Todo("Walk the dog", Done)
Core Functions
Displaying a Status
Pattern match on Status to produce a human-readable string:
func show_status(s: Status) -> String
example Pending -> "[ ]"
example Done -> "[x]"
match s with
| Pending -> return "[ ]"
| Done -> return "[x]"
end match
end func show_status
Displaying a Todo
Use the show_status helper and field access to format a single todo:
func show_todo(t: Todo) -> String
example Todo("Buy milk", Pending) -> "[ ] Buy milk"
example Todo("Walk dog", Done) -> "[x] Walk dog"
return show_status(t.status) + " " + t.title
end func show_todo
Adding a Todo
Build a new Todo with Pending status and append it to the list:
func add_todo(todos: List[Todo], title: String) -> List[Todo]
example [], "Buy milk" -> [Todo("Buy milk", Pending)]
let new_todo: Todo = Todo(title, Pending)
return append(todos, new_todo)
end func add_todo
append returns a new list -- lists in Geno are immutable.
Completing a Todo
Walk the list and flip the matching item to Done. If no item matches,
return the list unchanged:
func complete_todo(todos: List[Todo], title: String) -> List[Todo]
example [Todo("A", Pending)], "A" -> [Todo("A", Done)]
example [Todo("A", Pending)], "B" -> [Todo("A", Pending)]
return map(todos, fn(t: Todo) -> match t.title == title with
| true -> Todo(t.title, Done)
| false -> t
end match)
end func complete_todo
Filtering by Status
Use filter with a lambda that pattern-matches the status field:
func filter_by_status(todos: List[Todo], s: Status) -> List[Todo]
example [Todo("A", Pending), Todo("B", Done)], Pending -> [Todo("A", Pending)]
example [Todo("A", Pending), Todo("B", Done)], Done -> [Todo("B", Done)]
return filter(todos, fn(t: Todo) -> t.status == s)
end func filter_by_status
Counting by Status
Count is just the length of a filtered list. The pipeline operator |> makes
this read left to right:
func count_by_status(todos: List[Todo], s: Status) -> Int
example [Todo("A", Pending), Todo("B", Done), Todo("C", Pending)], Pending -> 2
example [Todo("A", Pending), Todo("B", Done), Todo("C", Pending)], Done -> 1
example [], Pending -> 0
return todos
|> filter(_, fn(t: Todo) -> t.status == s)
|> length(_)
end func count_by_status
The _ placeholder marks where the piped value is inserted.
Using Result Types
What if someone tries to complete a todo that does not exist? Rather than
silently returning the original list, we can use Result to signal the error.
func safe_complete(todos: List[Todo], title: String) -> Result[List[Todo], String]
example [Todo("A", Pending)], "A" -> Ok([Todo("A", Done)])
example [Todo("A", Pending)], "Z" -> Err("todo not found: Z")
let found: List[Todo] = filter(todos, fn(t: Todo) -> t.title == title)
if length(found) == 0 then
return Err("todo not found: " + title)
end if
return Ok(complete_todo(todos, title))
end func safe_complete
Callers can then pattern-match on the result:
func complete_and_report(todos: List[Todo], title: String) -> String
example [Todo("A", Pending)], "A" -> "Completed: A"
example [Todo("A", Pending)], "Z" -> "Error: todo not found: Z"
let result: Result[List[Todo], String] = safe_complete(todos, title)
match result with
| Ok(_) -> return "Completed: " + title
| Err(e) -> return "Error: " + e
end match
end func complete_and_report
Testing with Example Clauses
Every function above already has example clauses. Geno verifies these at
runtime -- they act as built-in tests. When you run or type-check a file, the
runtime confirms each example returns the expected value.
Example clauses follow a simple format:
example <arg1>, <arg2>, ... -> <expected_result>
A few guidelines:
- Cover the happy path and the edge case (empty list, missing item).
- Use constructors directly in examples:
Todo("A", Pending),Ok(5),Err("bad"). - Functions with three or more parameters use named arguments at the call site, but example clauses list values positionally.
If an example clause fails, Geno reports the function name, the input, the expected output, and the actual output, so you can fix the bug immediately.
Putting It All Together
Create Main.geno:
import Todo
func main() -> List[String]
var todos: List[Todo] = []
// Add some items
todos = add_todo(todos, "Buy milk")
todos = add_todo(todos, "Write tutorial")
todos = add_todo(todos, "Walk the dog")
// Complete one
todos = complete_todo(todos, "Buy milk")
// Build a summary
let pending: Int = count_by_status(todos, Pending)
let done: Int = count_by_status(todos, Done)
let pending_todos: List[Todo] = filter_by_status(todos, Pending)
var lines: List[String] = []
var i: Int = 0
while i < length(pending_todos) do
lines = append(lines, show_todo(pending_todos[i]))
i = i + 1
end while
let header: String = f"{to_string(done)} done, {to_string(pending)} pending"
return concat([header], lines)
end func main
Running and Checking
Type-check both files
geno check Main.geno
# Type check passed: Main.geno
# 2 modules
The checker follows the import Todo statement and validates Todo.geno as
well.
Run the program
geno run Main.geno --unsafe
# => ["1 done, 2 pending", "[ ] Write tutorial", "[ ] Walk the dog"]
Verify examples only
Every example clause is checked when you run the file. If any example does not
match, Geno prints a clear error message showing the expected versus actual
value.
Building an Interactive App
Once the core logic works, you can wrap it in an interactive GUI using Geno's
app mode. App mode programs define three special functions -- init, update,
and render -- and compile to static HTML/JS artifacts. Use --single-file
when you need one self-contained HTML file.
A minimal interactive todo app would look like this:
import Todo
type AppState = AppState(todos: Array[String], count: Int, max: Int)
func init() -> AppState
return AppState(array_new(size: 100, default: ""), 0, 100)
end func init
func update(state: AppState, dt: Float) -> AppState
// Handle mouse clicks and text input to add/complete todos
return state
end func update
func render(state: AppState) -> Unit
clear_screen("#1e1e2e")
draw_text(text: "My Todos", x: 50, y: 20, size: 24, color: "#cdd6f4")
// Draw todo items, buttons, etc.
end func render
Compile it:
geno build TodoApp.geno -o todo.html --width 800 --height 600 --title "Todo App"
Open todo.html in your browser and you have a graphical todo app. See the
examples/apps/ directory for complete interactive applications.
Next Steps
- Language Tour -- the full set of Geno features with runnable examples
- Building Your First App -- another step-by-step walkthrough covering JSON and modules
- Getting Started -- installation, the REPL, and capabilities
- Explore the examples/ directory for more programs including quicksort, word count, and interactive apps
Rendered from docs/guide/tutorial-todo-app.md in the Geno repository.