A language you can hold in your head.
Tau is dynamically typed, garbage collected, and compiles to bytecode that runs on a small virtual machine written in C. Go-flavoured syntax, errors as values, concurrency in the language, a standard library where every module does one thing.
time = import("time")
# A routine is an ordinary call with 'tau' in front of it.
worker = fn(id, jobs, results) {
for job = recv(jobs) {
time.Sleep(10)
send(results, "worker {id} did job {job}")
}
}
jobs = pipe()
results = pipe()
for w = 1; w <= 3; ++w {
tau worker(w, jobs, results)
}
for j = 1; j <= 6; ++j {
send(jobs, j)
}
close(jobs)
# recv blocks until something shows up.
for i = 0; i < 6; ++i {
println(recv(results))
}
close(results)
$ tau run workers.tau
worker 1 did job 1
worker 3 did job 3
worker 2 did job 2
worker 1 did job 4
worker 3 did job 5
worker 2 did job 6
Why Tau
Most languages grow. Every year another keyword, another special case, another way to do the thing you could already do. Tau goes the other way: it is small on purpose and means to stay that way.
There are no classes, no inheritance, no generics, no exceptions, no operator overloading, no decorators, no macros. There is one loop keyword, one function keyword, one way to make an object, one way to report a failure. What you can learn in an afternoon is the whole language, not a starter subset of it.
The trade is deliberate. You give up cleverness and you get code that reads the same whoever wrote it, and a runtime you can reason about: a compiler to bytecode, a VM in C, a garbage collector, routines that are cheap enough to spawn without thinking about it.
Concurrency in the language
Put tau in front of a call and it runs as a routine. Pipes are the channels: pipe(), send, recv, close. Buffered or not, they are the only synchronisation you need.
Errors are values
No exceptions, no stack unwinding. error("...") makes one, failed(x) checks for one, and the assignment can go inside the check so the value is right there.
No self, no this
An object is made with new() and its methods are closures that captured it. No receiver, no implicit binding, no keyword: one concept, closures, doing the work of two.
Programs that ship as one file
tau bundle writes an executable: your bytecode, every module it imports and every shared object it opens, on top of a runtime in C without a compiler in it. A few hundred kilobytes, nothing to install on the other side.
C without a wrapper
dlopen() opens a shared object and the dot on it is dlsym: a symbol can be called straight away. When the types matter, ffi.Func(sym, "double pow(double, double)") takes the C declaration as it is written in the header, and ffi.Export hands a tau function back for C to call. No binding layer to generate.
A stdlib that fits in your head
strings, list, maps, os, io, bufio, path, math, time, json, xml, regexp, net, http, exec, flag, csv, log, rand, utf8, crypto, sync, testing, encoding. Small modules, each doing one thing, all written in Tau and readable in an afternoon.
A taste
Values, functions and interpolation. The last expression of a function is its
result, and if is an expression too:
add = fn(a, b) { a + b }
fib = fn(n) {
if n < 2 {
return n
}
fib(n - 1) + fib(n - 2)
}
min = if 3 < 7 { 3 } else { 7 }
println("{add(9, 1)} {fib(20)} {min}")
Failure is a value you look at, not a control flow event you catch:
div = fn(n, d) {
if d == 0 {
return error("division by zero")
}
n / d
}
# The assignment goes inside failed(), so the value is there either way.
if failed(res = div(16, 2)) {
println("boom: {res}")
} else {
println("16 / 2 is {res}")
}
if failed(res = div(1, 0)) {
println("boom: {res}")
}
Objects are built, not declared. new() gives you an empty one and you fill it:
Counter = fn() {
c = new()
c.n = 0
c.Inc = fn() { c.n = c.n + 1 }
c.Value = fn() { c.n }
return c
}
c = Counter()
c.Inc()
c.Inc()
println(c.Value(), keys(c))
$ tau run counter.tau
2 [n, Inc, Value]
There is no self and no this, and nothing is missing: c.Inc closed over
c when it was made, so it knows the object the way any closure knows what it
captured. A method is therefore an ordinary value — it can be taken out of the
object, passed around, or replaced:
# No self, no this: a method is a closure that captured the object.
Queue = fn() {
q = new()
q.items = []
q.Push = fn(x) { q.items = append(q.items, x) }
q.Pop = fn() {
if len(q.items) == 0 {
return null
}
first = q.items[0]
q.items = slice(q.items, 1, len(q.items))
return first
}
q.Len = fn() { len(q.items) }
return q
}
q = Queue()
q.Push("a")
q.Push("b")
# A method is an ordinary value: take it out of the object and it still works,
# because what it closed over is the object, not the call it was reached by.
pop = q.Pop
println(pop(), q.Len())
# And it can be replaced, without the language needing a word for it.
loud = q.Push
q.Push = fn(x) { loud(string(x) + "!") }
q.Push("c")
println(q.Pop(), q.Pop())
$ tau run methods.tau
a 1
b c!
Install
Tau needs Go and GCC to build:
$ git clone --recurse-submodules https://github.com/NicoNex/tau
$ cd tau
$ make install
That puts tau in ~/.local/bin and the standard library in
~/.local/lib/tau, no root needed. PREFIX=/usr/local sudo make install for
a system wide one.
No compiler on the other machine? Every release also ships packages that need
none — a .deb, an .rpm, an Arch package, a Windows installer, and a plain
archive for everything else. Tau looks for its standard library next to the
binary before anywhere else, so an unpacked archive runs where it stands and a
tree moved somewhere new keeps working.
Then run a file, or start the REPL with no arguments at all:
$ tau run hello.tau
$ tau
Tau v2.1.0 on Linux
>>>