This tutorial walks you through Turmeric in 22 steps, one expression at a
time. Open the REPL with tur repl (or load the web REPL) and type along.
Each step shows exactly what to type, what to expect back, and a brief explanation. "Try it yourself" prompts invite you to experiment before moving on -- they are optional but recommended.
The companion prose guide (docs/guides/quickstart.md) covers the same ground
with more explanation if you prefer reading over typing.
tur replIf you get stuck: :doc <name> prints documentation for any symbol.
:help lists all meta-commands. A blank line abandons an incomplete
expression.
What you'll learn: how the REPL works and how arithmetic is written.
Type this:
(+ 1 2)
Expected output:
3
What happened: Turmeric uses prefix notation -- the operator comes first,
inside parentheses, followed by its arguments. (+ 1 2) means "add 1 and 2".
The REPL prints the result immediately.
REPL tip:
:helplists all meta-commands.:quitor Ctrl-D exits.Try it yourself: evaluate
(- 100 58)and(* 6 7). What does(/ 144 12)return?
What you'll learn: string literals, boolean values, equality, and println.
Type this:
(println "Hello, Turmeric!")
Expected output:
Hello, Turmeric!
Then try:
(= 1 1)
Expected output:
true
What happened: String literals are written in double quotes. println
prints a value followed by a newline and returns nil (which the REPL does not
print). = tests equality and returns a boolean.
Try it yourself: evaluate
(not true)and(not false). Print a greeting of your own.
letWhat you'll learn: how to introduce local names scoped to an expression.
Type this:
(let [x 10 y 20] (+ x y))
Expected output:
30
What happened: let binds names to values for the duration of its body.
The bindings x and y do not exist outside the let. You can have as many
bindings as you like in the same vector.
REPL tip: If you open a parenthesis and press Enter, the REPL keeps reading (shown with an indented prompt) until the expression is balanced. A blank line abandons an incomplete expression.
Try it yourself: bind three values
a,b,cand compute(+ a (* b c)).
defnWhat you'll learn: how to define a named function that persists across REPL expressions.
Type this:
(defn square [x :int] :int (* x x))
Then call it:
(square 9)
Expected output:
81
What happened: defn defines a named function. The parameter list
[x :int] gives each parameter a name and type annotation. The return type
:int follows the parameter list. Top-level definitions persist for the rest
of the REPL session.
REPL tip:
:doc squareprints the signature of a user-defined symbol.:type (square 3)shows the inferred return type without evaluating.Try it yourself: define a
cubefunction and call it with a few values.
if and condWhat you'll learn: conditional expressions; if always produces a value.
Type this:
(defn abs [n :int] :int
(if (< n 0) (- 0 n) n))
Then:
(abs -5)
Expected output:
5
Now try a multi-branch form:
(defn sign [n :int] :int
(cond
(> n 0) 1
(< n 0) -1
:else 0))
(sign -3)
Expected output:
-1
What happened: if takes a condition, a "then" expression, and an "else"
expression -- both branches must produce a value. cond chains tests in order
and evaluates the expression next to the first truthy test; :else is the
fallback.
Try it yourself: write a
clampfunction that takesn,lo, andhiand returnsloifn < lo,hiifn > hi, ornotherwise.
What you'll learn: writing recursive functions; base case and recursive case.
Type this:
(defn factorial [n :int] :int
(if (<= n 1) 1 (* n (factorial (- n 1)))))
Then:
(factorial 10)
Expected output:
3628800
What happened: factorial calls itself with a smaller argument until it
reaches the base case (<= n 1). Turmeric has no loop keyword -- iteration is
expressed through recursion or macros like for (step 13).
Try it yourself: write a recursive
fibfunction that computes Fibonacci numbers. Try(fib 30).
when and unlessWhat you'll learn: one-arm conditionals for side-effectful code.
Type this:
(when true (println "yes"))
Expected output:
yes
Then:
(unless false (println "also yes"))
Expected output:
also yes
What happened: when evaluates its body only when the condition is truthy
and returns nil otherwise. unless is the opposite -- it evaluates its body
when the condition is falsy. Both are macros defined in stdlib/macros.tur.
Try it yourself: use
whento print a warning only when a value is negative.
Option: constructors and predicatesWhat you'll learn: how to represent a value that may or may not be present.
Type this:
(option-some? (option-some 42))
Expected output:
true
Then:
(option-none? (option-none))
Expected output:
true
Then:
(option-some? (option-none))
Expected output:
false
What happened: option-some wraps a value in an optional container.
option-none represents the absence of a value. The predicates option-some?
and option-none? test which case you have. Evaluating (option-none) alone
produces a nil result, which the REPL does not print -- that is normal.
Try it yourself: evaluate
(option-none? (option-some 0)). Is asomeof zero stillsome?
Option: safe unwrappingWhat you'll learn: extracting a value from an Option without crashing,
and writing functions that return Option instead of panicking.
Type this:
(option-unwrap (option-some 99))
Expected output:
99
Then:
(option-unwrap-or (option-none) -1)
Expected output:
-1
Now define a division function that never crashes:
(defn safe-div [a :int b :int]
(if (= b 0) (option-none) (option-some (/ a b))))
(option-unwrap (safe-div 10 2))
Expected output:
5
(option-unwrap-or (safe-div 10 0) -1)
Expected output:
-1
What happened: option-unwrap extracts the inner value or panics if the
option is none. option-unwrap-or provides a fallback instead of panicking.
Returning Option from a function lets callers decide how to handle the
missing case.
Try it yourself: write
safe-headthat returns(option-none)for an empty vector and(option-some (vec-get v 0))otherwise. You will needvec-lenfrom step 12 to check emptiness.
Result: success or failureWhat you'll learn: the two-case Result type that carries either a success
value or an error value.
Type this:
(ok? (ok 100))
Expected output:
true
Then:
(err? (ok 100))
Expected output:
false
Then:
(result-unwrap-or (err 0) -1)
Expected output:
-1
What happened: ok wraps a success value; err wraps an error value.
ok? and err? test which case you have. result-unwrap-or extracts the
success value or returns a fallback when the result is an error. Like
option-none, evaluating (ok 100) or (err 0) alone prints nothing.
Try it yourself: rewrite
safe-divfrom step 9 to return(ok (/ a b))on success and(err 0)on division by zero.
Option and Result with condWhat you'll learn: composing predicates inside cond to dispatch on
wrapped values.
Type this:
(defn describe-result [r]
(cond
(ok? r) (println "ok!")
(err? r) (println "err!")
:else (println "unknown")))
(describe-result (ok 1))
Expected output:
ok!
Then:
(defn describe-option [o]
(cond
(option-some? o) (println "some!")
(option-none? o) (println "none!")
:else (println "unknown")))
(describe-option (option-none))
Expected output:
none!
What happened: cond with ok? / err? / option-some? / option-none?
gives you a clean, readable way to branch on wrapped values. This is the
standard lightweight dispatch pattern before reaching for full pattern matching.
Try it yourself: write
show-divthat callssafe-divand prints either the result or the string "division by zero".
What you'll learn: mutable growable arrays -- creating them, adding elements, and reading back by index.
Type this:
(let [v (vec-new)]
(vec-push! v 10)
(vec-push! v 20)
(vec-push! v 30)
(println (vec-len v))
(println (vec-get v 1)))
Expected output:
3
20
What happened: vec-new allocates an empty vector. vec-push! appends a
value in place (the ! suffix signals mutation). vec-len returns the current
length; vec-get reads by zero-based index.
Try it yourself: build a vector containing the integers 0 through 4 by calling
vec-push!five times, then print each element.
for macroWhat you'll learn: counted iteration with for; combining a loop with a
vector built in step 12.
Type this:
(for i 0 5 (println i))
Expected output:
0
1
2
3
4
What happened: (for i lo hi body) binds i to each integer in [lo,
hi) and evaluates body once per value. It is a macro -- :doc for shows
its signature. The index variable is available inside the body.
REPL tip:
:doc forshows the macro's parameter names and a usage example.Try it yourself: use
forto push the first five square numbers into a vector, then print the vector's length.
fnWhat you'll learn: anonymous functions that capture their surrounding scope.
Type this:
(let [add5 (fn [x :int] :int (+ x 5))]
(add5 10))
Expected output:
15
Then define a function that returns a closure:
(defn make-adder [n :int] (fn [x :int] :int (+ x n)))
(let [add3 (make-adder 3)] (add3 7))
Expected output:
10
What happened: fn creates an anonymous function. When make-adder runs,
the returned closure captures the value of n from the enclosing scope and
carries it along. Each call to make-adder produces a distinct closure with
its own n.
Try it yourself: write
make-multiplierthat returns a closure multiplying its argument byn.
What you'll learn: functions are first-class values that can be passed to and returned from other functions.
Type this:
(defn apply-twice [f x :int] :int (f (f x)))
(apply-twice (fn [x :int] :int (* x 2)) 3)
Expected output:
12
What happened: apply-twice accepts any function f and an integer x,
applies f to x, then applies f again to the result. The inline fn
doubles its argument, so 3 becomes 6 then 12.
Try it yourself: write
apply-nthat takes a function, a starting value, and a count, and applies the function that many times.
What you'll learn: grouping related values into a named record type.
Type this:
(defstruct Point [x :int y :int])
Then construct an instance and read its fields:
(let [p (Point 3 4)]
(println (Point-x p))
(println (Point-y p)))
Expected output:
3
4
What happened: defstruct declares a record type with named fields.
The constructor shares the struct's name. For each field f in struct S,
defstruct generates an accessor S-f. Struct values are allocated on the
heap and passed by pointer.
REPL tip:
:type (Point 1 2)confirms the inferred type of a struct expression.Try it yourself: define a
Rectstruct withwidth :intandheight :intfields. Write anareafunction that returns(* (Rect-width r) (Rect-height r)).
What you'll learn: how to declare a named effect and invoke it; what happens without a handler.
Type this:
(defeffect Log [msg :cstr] :void)
Then define a function that performs it:
(defn do-work [] :void
(perform (Log "starting"))
(perform (Log "done")))
Now call it without a handler:
(do-work)
Expected output:
error: unhandled effect Log
What happened: defeffect declares a named effect with a parameter list
and return type. perform raises the effect and suspends the current
computation, searching up the call stack for a matching handler. With none
present the runtime raises an error. This is intentional -- the handler comes
in the next step.
Try it yourself: declare a second effect
Warn [msg :cstr] :voidand call(perform (Warn "careful"))insidedo-work.
What you'll learn: writing a handle block that intercepts an effect and
resumes the computation.
Type this:
(handle (do-work)
(Log [msg] k)
(do (println msg) (resume k (nil-value))))
Expected output:
starting
done
What happened: handle wraps a computation and installs a handler clause
for Log. When do-work performs Log, execution jumps to the clause body.
msg receives the argument; k is the captured one-shot continuation.
(resume k (nil-value)) passes nil back to the perform expression and
continues do-work from where it left off.
REPL tip:
kis one-shot -- calling(resume k v)consumes it. Attempting to resume twice is a runtime error.Try it yourself: change the handler body to also print a counter alongside each message, e.g.
"[1] starting".
What you'll learn: swapping handlers to change behavior without modifying the computation.
Type this:
(handle (do-work)
(Log [msg] k)
(do (println (str-concat "[LOG] " msg)) (resume k (nil-value))))
Expected output:
[LOG] starting
[LOG] done
What happened: do-work is unchanged. Only the handler differs -- it now
prefixes each message with [LOG]. Effects separate the declaration of an
operation from its implementation, making it straightforward to swap in
logging, testing, or no-op handlers without touching the business logic.
Try it yourself: write a silent handler that discards log messages (calls
resumewithout printing). Confirm thatdo-workstill completes.
What you'll learn: an effect whose perform expression evaluates to a
value supplied by the handler.
Type this:
(defeffect Ask [] :int)
(defn use-ask [] :int
(+ 1 (perform (Ask))))
(handle (use-ask)
(Ask [] k) (resume k 41))
Expected output:
42
What happened: Ask has return type :int, so (perform (Ask))
evaluates to whatever the handler passes to resume. The handler supplies
41; use-ask adds 1, giving 42. The handler controls the value the
computation receives back from perform.
Try it yourself: change
(resume k 41)to(resume k 99)and observe the new result ofuse-ask.
:reloadWhat you'll learn: writing Turmeric code to a file and loading it into the current REPL session.
First, save the following to a file called hello.tur in your working
directory:
(defn greet [name :cstr] :void
(println (str-concat "Hello, " (str-concat name "!"))))
Then in the REPL:
:reload hello.tur
Expected output:
reloaded hello.tur
Now call the function:
(greet "world")
Expected output:
Hello, world!
What happened: :reload <file> evaluates the contents of the file into
the current session, making all its definitions available immediately. If the
file contains an error, a diagnostic is printed and the session continues. You
can edit the file and :reload again without restarting.
Try it yourself: add a second function to
hello.tur,:reloadit, and call the new function.
What you'll learn: nothing to type -- just a map of where to head next.
You have covered the core of Turmeric: expressions, functions, control flow,
Option, Result, vectors, closures, structs, and algebraic effects. Here
are the natural next stops depending on what interests you:
Deeper on today's topics
docs/guides/error-handling-guide.md -- Result, Option, panic, and
contract macros (assert!, require!, ensure!)docs/guides/effects-system-guide.md -- full effects reference: effect rows,
composing handlers, one-shot vs. cloneable continuationsConcurrency
docs/guides/threading-guide.md -- OS threads, Mutex, Atomic, channelsdocs/guides/async-await-guide.md -- async/await with fibersdocs/guides/stm-tutorial.md -- software transactional memoryType system
docs/guides/hkt-guide.md -- Functor, Monad, Applicative (higher-kinded
types)docs/guides/hrt-guide.md -- rank-2 polymorphic function parametersdocs/guides/gadts-guide.md -- generalized algebraic data typesdocs/guides/type-annotations-guide.md -- compound annotation syntaxModules and packages
docs/guides/module-system-guide.md -- namespacing and exportsdocs/guides/package-management-guide.md -- projects, spices, tur.lockAPI reference
docs/html/api/index.html -- generated reference from ;;; docstringsREPL tip:
:doc <sym>works for any symbol in scope. It is the fastest way to look something up without leaving the REPL.