A Racket language variant that uses significant tab-based indentation instead of parentheses, with Clojure-style persistent data structures.
tab-racket lets you write Racket code using indentation to denote structure, similar to Python or Haskell. Instead of wrapping expressions in parentheses, you indent child expressions under their parent.
- Tab-based significant indentation - Structure code without parentheses
- Line continuation -
\at end of line continues to next (same-indented) line - Infix operators -
(a ~+ b)for readable binary operations - Persistent vectors -
[]creates immutable, structurally-shared vectors with O(log32 n) operations - Persistent hash maps -
{}creates HAMTs (Hash Array Mapped Tries) with O(log64 n) operations - Self-evaluating keywords -
:foosymbols auto-evaluate like Clojure/Ruby/Elixir atoms - Typed Racket support -
#lang tab-racket/typedfor full type annotations - File extension - Use
.trk("tabbed racket") for tab-racket source files
#lang tab-racket
define (fib n)
cond
(= n 0) 0
(= n 1) 1
else
+
fib (- n 1)
fib (- n 2)
displayln
fib 10
This is equivalent to:
#lang racket
(define (fib n)
(cond
[(= n 0) 0]
[(= n 1) 1]
[else
(+
(fib (- n 1))
(fib (- n 2)))]))
(displayln
(fib 10))cd tab-racket
raco pkg installThis project uses Nix flakes for reproducible development:
nix developCreate a file with the #lang tab-racket directive:
#lang tab-racket
displayln "Hello, tabs!"
Run it with Racket:
racket hello.trk- Tabs only - Spaces for indentation are rejected with an error
- Nesting - Each tab level increases nesting depth
- Siblings - Lines at the same indentation level are siblings
- Children - Lines indented one level deeper become children of the preceding line
| Tab-indented code | S-expression |
|---|---|
foo |
foo |
+ 1 2 |
(+ 1 2) |
define x 1 |
(define x 1) |
define (f x)+ x 1 |
(define (f x) (+ x 1)) |
When multiple tokens appear on the same line, they form a list:
+ 1 2 ; becomes (+ 1 2)
define x 1 ; becomes (define x 1)
Indented children are appended to their parent's token list:
define (add a b) ; parent line has tokens: define, (add a b)
+ a b ; child line adds: (+ a b)
; result: (define (add a b) (+ a b))
Empty lines and lines containing only whitespace are ignored.
tab-racket supports Clojure-style syntax for persistent vectors and hash maps. These are immutable by default with efficient structural sharing:
| Syntax | Data Structure | Complexity |
|---|---|---|
[a b c] |
Persistent Vector (PVector) | O(log32 n) |
{k1 v1 k2 v2} |
Persistent Hash Map (HAMT) | O(log64 n) |
![a b c] |
Mutable Racket vector | O(1) |
!{k1 v1 k2 v2} |
Mutable Racket hash | O(1) amortized |
#lang tab-racket
; Persistent by default - updates return new structure, original unchanged
define scores {"alice" 95 "bob" 87}
define updated-scores (hamt-set scores "carol" 92)
; scores still has only alice and bob!
define nums [1 2 3]
define more-nums (pvector-push nums 4)
; nums is still [1 2 3]!
; Mutable when you need it
define mutable-vec ![1 2 3]
vector-set! mutable-vec 0 99
Note: Since [] now creates persistent vectors, use parentheses () for list operations like let bindings.
Keywords (symbols starting with :) are automatically self-evaluating, like atoms in Clojure, Ruby, or Elixir:
#lang tab-racket
;; Keywords evaluate to themselves - no quoting needed!
define person {:name "Alice" :age 30}
displayln (hamt-ref person :name) ; prints "Alice"
;; Works in function calls and comparisons
define (greet who)
if (equal? who :world)
displayln "Hello, World!"
displayln (format "Hello, ~a!" who)
greet :world
Note: Bare : is reserved for type annotations in tab-racket/typed and is not auto-quoted.
For binary operations, you can use infix syntax with the ~ prefix. (a ~func b) transforms to (func a b):
#lang tab-racket
;; Arithmetic - more readable than prefix
displayln (1 ~+ 2) ; → (+ 1 2) = 3
displayln (10 ~- 3) ; → (- 10 3) = 7
displayln (4 ~* 5) ; → (* 4 5) = 20
displayln (10 ~/ 2) ; → (/ 10 2) = 5
;; Comparisons
displayln (5 ~< 10) ; → (< 5 10) = #t
displayln (5 ~> 10) ; → (> 5 10) = #f
displayln (5 ~= 5) ; → (= 5 5) = #t
;; Any binary function works
displayln (17 ~modulo 5) ; → (modulo 17 5) = 2
displayln ("hello" ~string-append " world") ; → "hello world"
;; Nested expressions - use parens for precedence
displayln (1 ~+ (2 ~* 3)) ; → (+ 1 (* 2 3)) = 7
displayln ((10 ~- 2) ~* 3) ; → (* (- 10 2) 3) = 24
;; Works in function bodies
define (add x y)
x ~+ y
define (quadratic a b c x)
(a ~* (x ~* x)) ~+ ((b ~* x) ~+ c)
Works with #lang tab-racket/typed too:
#lang tab-racket/typed
: add (Integer Integer ~-> Integer)
define (add x y)
x ~+ y
: quadratic (Integer Integer Integer Integer ~-> Integer)
define (quadratic a b c x)
(a ~* (x ~* x)) ~+ ((b ~* x) ~+ c)
displayln (add 3 4) ; 7
displayln (quadratic 1 2 1 3) ; 16
Type annotation syntax: In type annotations, ~-> as the second-to-last element moves to the front as ->. This reads naturally as "args ~-> return":
(Integer ~-> Integer) ; → (-> Integer Integer)
(Integer Integer ~-> Integer) ; → (-> Integer Integer Integer)
(String (Listof Integer) ~-> Integer) ; → (-> String (Listof Integer) Integer)
((Integer ~-> Integer) Integer ~-> Integer) ; Higher-order function
Note: The ~ prefix is only special when followed by an identifier. The tilde character in format strings (~a, ~s, etc.) is unaffected.
Long lines can be continued using \ at the end of a line. All leading whitespace on continuation lines is stripped, allowing flexible visual alignment:
#lang tab-racket
;; Align args visually with spaces
define result (some-long-function-name "first-arg" \
"second-arg" \
"third-arg")
;; Multiple continuations chain together
define message (string-append "This is a very " \
"long string that " \
"spans multiple lines")
;; Works in function bodies too
define (process-data data)
transform data \
filter-valid \
aggregate
Rules:
\must be the last non-whitespace character on the line- All leading whitespace (tabs/spaces) on continuation lines is stripped
\at EOF is an error (nothing to continue to)\inside strings (like"\n") is not treated as continuation
; Construction
[1 2 3] ; literal syntax
(pvector 1 2 3) ; function call
(list->pvector '(1 2 3)) ; from list
; Operations (all O(log32 n))
(pvector-ref v 0) ; get element at index
(pvector-set v 0 99) ; return new vector with updated element
(pvector-push v 4) ; return new vector with element appended
(pvector-pop v) ; return new vector without last element
(pvector-length v) ; get length
; Conversion
(pvector->list v) ; convert to list
(pvector->vector v) ; convert to Racket vector; Construction
{"name" "Alice" "age" 30} ; literal syntax
(hamt "name" "Alice") ; function call
(list->hamt '(("a" . 1))) ; from alist
; Operations (all O(log64 n))
(hamt-ref h "name") ; get value for key
(hamt-ref h "x" 'default) ; with default value
(hamt-set h "city" "NYC") ; return new hamt with key-value
(hamt-remove h "age") ; return new hamt without key
(hamt-count h) ; get number of entries
(hamt-contains? h "name") ; check if key exists
; Conversion
(hamt->list h) ; convert to alist
(hamt-keys h) ; get all keys
(hamt-values h) ; get all values#lang tab-racket
displayln "Hello, tabs!"
#lang tab-racket
define (fib n)
cond
(= n 0) 0
(= n 1) 1
else
+
fib (- n 1)
fib (- n 2)
displayln
fib 10
#lang tab-racket
define (abs x)
if (< x 0)
- x
x
displayln
abs -5
#lang tab-racket
let
((x 10) (y 20))
+ x y
Note: Use parentheses () for let bindings since [] creates vectors in tab-racket.
This example demonstrates persistent vectors and HAMTs:
#lang tab-racket
define sample-text "the quick brown fox jumps over the lazy dog the fox was quick"
;; Split string into words
define split-words
lambda (text)
regexp-split #rx" +" (string-downcase text)
;; Count words using HAMT
define count-words
lambda (words)
foldl
lambda (word freq-map)
define current (hamt-ref freq-map word 0)
hamt-set freq-map word (+ current 1)
{}
words
;; Demo persistence
define v1 [1 2 3]
define v2 (pvector-push v1 4)
displayln (format "v1 = ~a" (pvector->list v1))
displayln (format "v2 = ~a" (pvector->list v2))
displayln "v1 is UNCHANGED! That's persistence."
;; Run word count
define words (split-words sample-text)
define freq (count-words words)
displayln (format "Word frequencies: ~a" (hamt->list freq))
The examples include implementations of production cache algorithms using typed persistent data structures:
ARC (Adaptive Replacement Cache) - examples/arc-cache.trk
- Self-tuning algorithm used in ZFS
- Balances recency (LRU) and frequency (LFU) using ghost lists
- Adapts to workload patterns automatically
S3-FIFO (Simple, Scalable, FIFO-based) - examples/s3fifo-cache.trk
- From SOSP 2023: "FIFO Queues are All You Need for Cache Eviction"
- Uses two FIFO queues (S=10%, M=90%) to filter one-hit-wonders
- Excellent scan resistance for sequential access patterns
SIEVE (1-bit) - examples/sieve-cache.trk
- From NSDI 2024: "SIEVE is Simpler than LRU"
- Single FIFO queue with a scanning "hand" pointer
- Each entry has a 1-bit visited flag; unvisited items evicted first
- Key insight: retained items stay in place (unlike CLOCK which reinserts)
SIEVE-2 (2-bit) - examples/sieve2-cache.trk
- Variant using a 2-bit counter (0-3) instead of a boolean
- On hit: increment counter (saturates at 3)
- On hand pass: decrement counter; evict when 0
- Gives frequently-accessed items more "staying power"
Run the comprehensive benchmark (default: 50,000 ops per test):
racket tests/cache-bench.trk
# Configure via environment variables:
BENCH_OPS=100000 racket tests/cache-bench.trk # More ops for statistical confidence
BENCH_SEED=42 racket tests/cache-bench.trk # Reproducible random seed
BENCH_OPS=10000 BENCH_SEED=42 racket tests/cache-bench.trk # Both| Test Pattern | LRU | ARC | S3-FIFO | SIEVE | SIEVE-2 |
|---|---|---|---|---|---|
| Temporal Locality | 85.1% | 85.1% | 84.8% | 81.6% | 78.3% |
| Hot/Cold (80/20) | 83.9% | 84.0% | 84.0% | 84.0% | 84.0% |
| Scan Resistance | 25.3% | 29.5% | 29.8% | 30.0% | 29.9% |
| Zipf Distribution | 31.7% | 34.5% | 37.7% | 39.8% | 41.8% |
| Loop/Burst | 56.0% | 66.2% | 69.5% | 69.9% | 70.0% |
| Working Set Shift | 75.4% | 75.4% | 71.2% | 63.4% | 58.1% |
| Mixed Recency/Freq | 50.0% | 53.0% | 53.0% | 53.0% | 53.0% |
Takeaways:
- SIEVE/SIEVE-2 excel at Zipf (power-law) patterns - common in real workloads
- ARC wins on mixed workloads that alternate between recency and frequency patterns
- S3-FIFO strong on hot/cold and loop patterns thanks to ghost queue filtering one-hit-wonders
- LRU strong baseline for pure temporal locality patterns
- Results vary with random seed - run multiple times for statistical confidence
- Real-world traces: SIEVE ~6% better than LRU (NSDI'24), S3-FIFO lowest miss ratio on 10/14 datasets (SOSP'23)
- ARC algorithm per Megiddo & Modha, FAST 2003
raco test tests/tab-racket-test.rkt tests/examples-test.rktOr run all tests in the tests directory:
raco test tests/.
├── tab-racket/
│ ├── main.rkt # Core reader and parser (untyped)
│ ├── typed/
│ │ └── main.rkt # Typed variant reader
│ ├── lang/
│ │ └── reader.rkt # #lang tab-racket support
│ └── info.rkt # Package metadata
├── hamt/
│ ├── main.rkt # HAMT implementation (untyped)
│ ├── typed-main.rkt # HAMT with full type annotations
│ └── optimized-main.rkt # HAMT with unsafe ops for speed
├── pvector/
│ ├── main.rkt # PVector implementation (untyped)
│ └── typed-main.rkt # PVector with full type annotations
├── examples/
│ ├── hello.trk # Hello world example
│ ├── fibonacci.trk # Fibonacci example
│ ├── fibonacci-typed.trk # Typed Fibonacci
│ ├── word-freq.trk # Word frequency counter
│ ├── counter-typed.trk # Word counter with types
│ ├── arc-cache.trk # ARC cache algorithm (typed)
│ ├── s3fifo-cache.trk # S3-FIFO cache algorithm (typed)
│ ├── sieve-cache.trk # SIEVE 1-bit cache (typed)
│ └── sieve2-cache.trk # SIEVE 2-bit cache (typed)
├── tests/
│ ├── tab-racket-test.rkt # Parser test suite
│ ├── hamt-test.rkt # HAMT test suite
│ ├── pvector-test.rkt # PVector test suite
│ ├── hash-api-test.trk # HAMT API demo (tab-racket)
│ ├── pvector-api-test.trk # PVector API demo (tab-racket)
│ ├── examples-test.rkt # Integration tests for examples
│ └── cache-bench.trk # Comprehensive cache algorithm benchmark
├── flake.nix # Nix flake configuration
├── docs/
│ └── PROJECT_PLAN.md # Project planning document
└── README.md
For type-annotated tab-racket code, use the typed variant:
#lang tab-racket/typed
;; Type annotations use : prefix (like Typed Racket)
: fib (Integer ~-> Integer)
define (fib n)
cond
(= n 0) 0
(= n 1) 1
else
+ (fib (- n 1)) (fib (- n 2))
displayln (fib 10)
Both persistent data structures have fully typed versions that can be used directly from tab-racket/typed code without contract overhead:
#lang tab-racket/typed
require "hamt/typed-main.rkt" "pvector/typed-main.rkt"
;; Types are: HAMT, PVector
: my-map HAMT
define my-map (hamt "key" 42)
: my-vec PVector
define my-vec (pvector 1 2 3)
The typed versions use unsafe-fxpopcount and other unsafe operations internally for performance while maintaining type safety at the API boundary.
The persistent data structures are designed for both correctness and performance:
| Operation | PVector | HAMT |
|---|---|---|
| Lookup | O(log32 n) ~1.1μs | O(log64 n) ~1.2μs |
| Insert/Update | O(log32 n) ~2.5μs | O(log64 n) ~3μs |
| Structural sharing | Yes | Yes |
At 100K elements, persistent operations are ~1.5-2x slower than mutable equivalents but provide immutability guarantees. The optimized HAMT version uses unsafe operations in hot paths for ~30% speedup.
- Tabs only - Spaces for indentation cause an error
- Full Racket semantics apply after parsing
- Persistent vectors use 32-way branching (max ~1 billion elements efficiently)
- Use
.trkextension for tab-racket files to distinguish from standard.rktfiles
MIT