Saffron is Turmeric with the type annotations made optional. One line at the
top of a file switches it on, and an unannotated parameter or return becomes
any instead of int:
#lang saffron
(defn double [x] (* x 2))
(defn main []
(println (double 21)) ; 42
(println (double 3.55)) ; 7.1
0)
One double, two argument types, no annotations. It is the same compiler, the
same runtime, and the same object files as typed Turmeric -- a Saffron module
and a Turmeric module link together in one program.
This guide is the tour: what writing Saffron feels like, start to finish,
for someone new to the Turmeric ecosystem. It assumes nothing but a browser.
For the reference -- exactly what any supports, how the boundary with typed
code is checked, and the one feature Saffron gives up -- read
saffron-guide.md afterwards.
Every example below appears twice: once in ordinary s-expressions
(#lang saffron), once in sweet-expression syntax (#lang saffron/sweet).
The two are the same program -- pick whichever you prefer and stay with it.
The #lang line is shown only on complete programs; the fragments in between
assume it is at the top of the file.
You do not have to install anything to follow along.
Open Try Turmeric in a browser. It runs the real compiler, built to WebAssembly, in the page -- nothing is sent to a server. Open the Language menu in the toolbar and pick a dialect:
saffron -- s-expressions, the left-hand column below.saffron/sweet -- sweet-expressions, the right-hand column.Then paste a program and run it. The Docs panel has every guide and the whole API reference, precached, so it works offline too.
Prefer a terminal? Install the compiler with the version manager:
curl -sSf https://turmeric-lang.com/install | sh
That installs tvm and the latest release; tvm install <version>
and tvm use <version> switch between releases later.
Homebrew, prebuilt binaries, Docker and build-from-source are all covered in the releases and installation guide.
Then write your first file and run it:
cat > hello.tur <<'EOF'
#lang saffron
(defn main []
(println "Hello from Saffron!")
0)
EOF
tur run hello.tur
The sweet-expression version is the same program with the outer parentheses
replaced by indentation. Save it as hello.tur.sweet (or keep the .tur
extension -- the #lang line is what selects the dialect):
cat > hello.tur.sweet <<'EOF'
#lang saffron/sweet
defn main []
println("Hello from Saffron!")
0
EOF
tur run hello.tur.sweet
Both print Hello from Saffron!. main returns 0 -- the process exit code.
tur run compiles and executes in one step. tur build hello.tur leaves an
executable behind instead, and tur repl gives you an interactive prompt.
println prints a value and a newline. In Saffron it works on a value whose
type is only known at run time, which is what lets a function print whatever
it is handed:
(defn show [x]
(println x))
(show 42) ; 42
(show 7.1) ; 7.1
(show "hi") ; hi
(show true) ; true
x here is an any -- a two-word box carrying a type tag and a payload --
and println reads the tag to decide how to print. Ints, floats, booleans and
strings all print. A payload with no printer, a keyword for instance, panics
naming the type (println: no operator for a Sym argument) rather than
printing something wrong.
Note that the default applies to parameters and returns, not to
expressions. The literal 7.1 written directly in a Saffron file is still a
float; it becomes an any on the way into show.
if is an expressionBoth branches are required, and the form produces a value:
(defn abs [n]
(if (< n 0) (- 0 n) n))
(abs -5) ; => 5
(abs -7.1) ; => 7.1
nil and false are falsyThis is the Lisp and Clojure convention, not C's. 0, "" and an empty
container are all truthy:
(defn describe [x]
(if x "truthy" "falsy"))
(describe 0) ; => "truthy"
(describe "") ; => "truthy"
(describe false) ; => "falsy"
(describe nil) ; => "falsy"
Deciding truthiness needs the value's runtime tag, which is why this is a
Saffron rule: in typed Turmeric an if condition must already be a bool.
condcond takes test/result pairs in order. else is the fallback:
(defn classify [n]
(cond (> n 0) "positive"
(< n 0) "negative"
else "zero"))
(classify 7.1) ; => "positive"
(classify -3) ; => "negative"
(classify 0) ; => "zero"
when and unless(when (> n 0) (println "positive"))
(unless (= n 0) (println "non-zero"))
Each takes exactly one body form. For several statements, wrap them in a
do:
(when (> n 0)
(do (println "positive")
(println n)))
There is no counted for. A loop is either recursion:
(defn countdown [n]
(when (> n 0)
(do (println n)
(countdown (- n 1)))))
(countdown 3) ; prints 3, 2, 1
or while with a ^mut binding you advance by hand:
(let [^mut i 0]
(while (< i 3)
(println i)
(set! i (+ i 1))))
; prints 0, 1, 2
^mut is what makes a binding writable; without it set! is rejected, so the
one mutable thing in the loop is visibly marked.
defn names a function. In Saffron the parameter list is just names:
(defn double [x] (* x 2))
(defn add [a b] (+ a b))
(double 21) ; => 42
(add 1 2) ; => 3
(add 1.5 2.25) ; => 3.75
Arithmetic on an any is numeric only. (+ "a" "b") is not string
concatenation -- it panics with +: no operator for a cstr argument.
fn builds a function value that captures its enclosing scope:
(defn make-adder [n]
(fn [x] (+ x n)))
(let [add7 (make-adder 7)]
(add7 35)) ; => 42
A function value is just another thing an any can hold, so higher-order code
needs no annotations either:
(defn apply-twice [f x]
(f (f x)))
(apply-twice double 3) ; => 12
(apply-twice (fn [n] (+ n 1)) 10) ; => 12
A [...] literal builds a vector whose elements are any, so it can be
heterogeneous. The Saffron prelude ships vec-map, vec-filter and
vec-fold over it, alongside the ordinary vec-len and vec-get:
(defn sum [v]
(vec-fold v 0 (fn [acc x] (+ acc x))))
(sum [1 2 3 4]) ; => 10
(vec-len [1 "two" 3.5]) ; => 3
(vec-get (vec-map [1 2 3] double) 2) ; => 6
(vec-len (vec-filter [1 2 3 4] (fn [x] (> x 2)))) ; => 2
vec-filter uses the same truthiness rule as if: only nil and false
drop an element.
A struct is all of several fields at once; an ADT is one of several
shapes. defdata declares one, match takes it apart:
(defdata Shape :copy
(Circle :float)
(Rect :float :float))
(defn area [s]
(match s
(Circle r) (* 3.14159 (* r r))
(Rect w h) (* w h)))
(area (Circle 2.5)) ; => 19.6349
(area (Rect 3.5 4.0)) ; => 14
Note what happened to s: it arrives as an any, and the arms name Shape,
so match narrows the box to that type before dispatching on the tag. A
value of some other type panics at the narrow (cast: any holds float, not Shape) rather than taking a wrong arm.
:copy after the name is the value discipline. Without it an ADT value is
single-use -- passing it somewhere moves it, and a second use is a compile
error -- which is the right default for a value owning a resource. Shape is
immutable and owns nothing, so :copy lets it be used as many times as you
like -- which the program at the end of this guide needs, since it asks each
shape for both its name and its area.
Constructors may carry nothing at all, and each arm binds its own payload:
(defdata Json
(JNull)
(JBool :bool)
(JNum :float)
(JStr :cstr))
(defn render [j]
(match j
(JNull) "null"
(JBool b) (if b "true" "false")
(JNum n) "a number"
(JStr s) s))
(render (JNull)) ; => "null"
(render (JBool false)) ; => "false"
(render (JStr "hi")) ; => "hi"
The constructors' payload types are annotated, and that is deliberate: an ADT declaration is the shape of your data, which is exactly the edge worth pinning down even in a dynamic file. The functions over it stay unannotated.
If you want to ask a value what it is rather than match on it, type-of names
the type -- it needs an any, so route the value through an unannotated
parameter:
(defn kind-of [x] (type-of x))
(kind-of 7.1) ; => "float"
(kind-of "hi") ; => "cstr"
(kind-of (JNull)) ; => "Json"
is? tests a type and cast narrows to one with a runtime check. See
sum-types-guide.md for parametric sums, record-style
variants and exhaustiveness, and saffron-guide.md for the
type-case idiom that is? enables.
A typeclass is an interface a type can implement. defclass declares the
methods; definstance supplies them for one type:
(defclass Speaks [a]
(speak [x] : cstr))
(definstance Speaks [int] (speak [x] "beep"))
(definstance Speaks [float] (speak [x] "boop"))
(definstance Speaks [cstr] (speak [x] "echo"))
(defn tell [x]
(println (.speak x)))
(tell 42) ; beep
(tell 7.35) ; boop
(tell "hi") ; echo
tell takes one any and reaches three different instances. The method name
.speak resolves at compile time -- that is what fixes the class and the slot
-- and only the instance waits for runtime, chosen from the box's own tag.
Your own types join in the same way:
(defdata Animal
(Dog)
(Cat))
(definstance Speaks [Animal]
(speak [x] (match x (Dog) "woof" (Cat) "meow")))
(tell (Dog)) ; woof
(tell (Cat)) ; meow
A type with no instance of the class panics naming both, rather than returning a wrong answer:
panic: no instance of Speaks for bool (dispatching .speak on an any)
Runtime dispatch is keyed on the box tag, so it covers methods that take only
the receiver -- which is most of them. A method taking a second argument of
the class's own type variable (eq [x : a y : a]), or returning one, needs a
type the tag cannot supply and panics saying so; narrow with is? or cast
first in those cases. saffron-guide.md lists the exact
set, and typeclass-guide.md covers constraints,
superclasses, defaults and the stdlib classes.
Animal's constructors happen to carry nothing, but that is not a
restriction: an ADT whose constructors carry payloads dispatches the same way,
and the program at the end of this guide does exactly that with Shape.
Effects separate what an operation is from how it is carried out. A
computation performs an effect; a handle installed by the caller decides
what actually happens.
defeffect declares one. Its result type is required -- an effect is a
protocol between a computation and its handler, and the handler has to know
what to resume with. Its parameters take the file's own default like any
other, so leaving them off means any; annotate one when you want that edge
pinned down:
(defeffect Log [msg : cstr] : int)
(defeffect Ask [] : int)
perform raises the effect and suspends. Everything else is ordinary
unannotated Saffron:
(defn work []
(perform (Log "starting"))
(let [n (perform (Ask))]
(perform (Log "done"))
(+ n 1)))
handle supplies a clause per effect. k is the continuation:
(resume k v) sends v back to the perform and carries on:
(handle (work)
(Log [msg] k) (do (println msg) (resume k 0))
(Ask [] k) (resume k 41))
; prints:
; starting
; done
; => 42
work never named a logger or a source of numbers. Swap the handler and the
same function behaves differently, with no edit to work:
(handle (work)
(Log [msg] k) (resume k 0)
(Ask [] k) (resume k 1))
; prints nothing
; => 2
That is dependency injection with no framework: logging, configuration, mocked I/O in a test, a retry policy. Deep and shallow handlers, effect rows and one-shot versus cloneable continuations are in effects-system-guide.md.
One complete program using the pieces above:
#lang saffron
(defdata Shape :copy
(Circle :float)
(Rect :float :float))
(defclass Describe [a]
(label [x] : cstr))
(definstance Describe [Shape]
(label [x] (match x (Circle r) "circle" (Rect w h) "rect")))
(defeffect Report [line : any] : int)
(defn area [s]
(match s
(Circle r) (* 3.14159 (* r r))
(Rect w h) (* w h)))
(defn survey [a b]
(do (perform (Report (.label a)))
(perform (Report (.label b)))
(+ (area a) (area b))))
(defn main []
(println
(handle (survey (Circle 2.5) (Rect 3.5 4.0))
(Report [line] k) (do (println line) (resume k 0))))
0)
Both print:
circle
rect
33.6349
Only the declarations -- the shape of the data, the class's method signature,
and the effect -- carry annotations. area, survey and main are all plain
names, and survey handles a circle and a rectangle without ever naming either
type: .label picks the instance from each value's own tag at run time.
handle finds the performs in the whole computation it wraps, however deep
-- survey performs directly here, but a perform several calls down is caught
just the same. That is worth saying because it did not always hold in a Saffron
file, and the fix is recent.
with-region).Option, Result and structs.#lang bases and layers; the full
sweet-expression rules.tur repl, and how it picks up a project.any, unions and gradual typing on the typed side.Examples in this guide are compiled and run by
tests/fixtures/docs-introducing-saffron-examples, and every
s-expression/sweet-expression pair is checked to read to the same AST by
tools/check-guide-pairs.tur.