A translation guide: each section shows the same program in a language
you already know, then in EigenScript. Every EigenScript block followed
by an output block is executed by the test suite
(tests/test_doc_examples.py) and must produce exactly that output.
The other languages' blocks are illustrative only.
The one-paragraph summary: EigenScript reads like Python (indentation
blocks, dynamic types), applies functions with a keyword like a
concatenative language (f of x), is a Lisp-like single-value-kind
world underneath (everything is one of eight runtime types, code runs
top to bottom, no static pass), and differs from all of them in one
fundamental way: every assignment is observed, and the language has
first-class constructs (converged, what is x, prev of x) for
asking the running program about its own state and history.
| Python | JavaScript | Rust | Lisp | EigenScript | |
|---|---|---|---|---|---|
| typing | dynamic | dynamic | static | dynamic | dynamic |
| numbers | int + float | float | many | many | one: 64-bit float |
| assignment | x = 1 |
let x = 1 |
let x = 1; |
(define x 1) |
x is 1 |
| call | f(x) |
f(x) |
f(x) |
(f x) |
f of x |
| blocks | indentation | braces | braces | parens | indentation |
| booleans | True/False |
true/false |
bool |
#t/#f |
1/0 |
| null | None |
null/undefined |
Option |
nil |
null |
| errors | exceptions | exceptions | Result |
conditions | try/catch |
| self-inspection | none | none | none | macros | interrogatives + observer |
Python:
x = 42
x += 1
print(x) # 43
print(7 / 2) # 3.5
print(7 // 2) # 3 (floor division operator)EigenScript — one number type, true division; floor explicitly:
x is 42
x += 1
print of x
print of (7 / 2)
print of (floor of (7 / 2))
43
3.5
3
Python:
def add(a, b):
return a + b
print(add(3, 4))JavaScript:
const add = (a, b) => a + b;
console.log(add(3, 4));EigenScript — a bare literal list after of is the argument list, at
every element count: f of [a, b] is two arguments, f of [x] is one.
(Parentheses always mean one argument: f of ([x]) passes a literal
1-element list whole.)
define add(a, b) as:
return a + b
print of (add of [3, 4])
inc is (x) => x + 1
print of (inc of 41)
7
42
Python:
def make_counter():
count = 0
def step():
nonlocal count
count += 1
return count
return stepJavaScript:
function makeCounter() {
let count = 0;
return () => ++count;
}EigenScript — no nonlocal needed: assignment is outward-mutable, so
inner functions write outer variables directly:
define make_counter as:
count is 0
define step as:
count is count + 1
return count
return step
c is make_counter of null
print of (c of null)
print of (c of null)
1
2
Python:
xs = [1, 2, 3, 4, 5]
print([v * v for v in xs])
print([v for v in xs if v % 2 == 0])
print(list(map(lambda v: v * 10, xs)))EigenScript — same comprehension shape; map/filter come from the
standard library:
load_file of "lib/list.eigs"
xs is [1, 2, 3, 4, 5]
print of [v * v for v in xs]
print of [v for v in xs if v % 2 == 0]
print of (map of [xs, (v) => v * 10])
[1, 4, 9, 16, 25]
[2, 4]
[10, 20, 30, 40, 50]
JavaScript:
const user = { name: "Ada", year: 1815 };
user.field = "computing";
console.log(user.name, Object.keys(user).length);EigenScript — dot access and bracket access are interchangeable
(keyword-named fields too: d.loop, d.when — like JavaScript member
access, the position disambiguates); assignment creates keys:
user is {"name": "Ada", "year": 1815}
user.field is "computing"
print of user.name
print of (len of (keys of user))
Ada
3
Rust:
match code {
200 => println!("OK"),
404 => println!("Not Found"),
_ => println!("other"),
}EigenScript:
code is 404
match code:
case 200:
print of "OK"
case 404:
print of "Not Found"
case _:
print of "other"
Not Found
Python:
try:
raise ValueError("boom")
except Exception as e:
print(f"caught: {e}")EigenScript — throw of raises, catch name: binds. A thrown value
binds unchanged; a built-in runtime error binds a {kind, message, line} dict with kind from a closed vocabulary (no exception
hierarchy — discriminate on the kind string):
try:
throw of "boom"
catch e:
print of f"caught: {e}"
try:
x is [1] - 1
catch e:
print of f"caught: {e.kind} at line {e.line}"
caught: boom
caught: type_mismatch at line 7
Clojure:
(-> 5 double inc) ; threading macroEigenScript — |> is built in:
double is (x) => x * 2
inc is (x) => x + 1
print of (5 |> double |> inc)
11
Python (threads + queue):
import threading, queue
q = queue.Queue()
threading.Thread(target=lambda: q.put(21 * 2)).start()
print(q.get())EigenScript — spawn + channels:
ch is channel of null
spawn of [(v) => send of [ch, v * 2], 21]
print of (recv of ch)
42
Before the metaphysics, the everyday win. Every numeric fixed-point loop in Python or JavaScript hand-rolls the same three things — and beginners get them wrong: an epsilon threshold, a way to remember the previous value(s), and a max-iteration guard so a non-converging loop doesn't hang.
# Python: the epsilon, the remembering, and the guard are all yours
def newton_sqrt(n, eps=1e-12, max_iter=1000):
guess = n / 2 or 1.0
for _ in range(max_iter): # max-iter guard, by hand
nxt = (guess + n / guess) / 2
if abs(nxt - guess) < eps: # epsilon threshold, by hand
return nxt
guess = nxt # remember the last value, by hand
return guess # ...and hope it convergedEigenScript watches every value settle for you, so the loop is the convergence test — no epsilon, no remembered previous, no guard:
define newton_sqrt as:
guess is n / 2.0
if guess == 0:
guess is 1.0
loop while not converged:
guess is (guess + n / guess) / 2.0
return guess
print of ("sqrt(2) = " + (str of (newton_sqrt of 2)))
sqrt(2) = 1.414213562373095
converged reads the loop's last-assigned value and is true once its trend has
flattened. (Use it inside a function so the loop gets a fresh binding to watch —
see examples/observer_vs_boilerplate.eigs.) The next section is why this
works; you can reach for it long before you need the theory.
Every assignment (outside unobserved blocks) updates an observer
tracking the value's entropy and trend. You can ask a variable about
itself, terminate loops on convergence instead of a hand-written
epsilon test, and read a variable's past:
x is 10
x is 20
x is 30
print of (what is x)
print of (who is x)
print of (when is x)
print of (prev of x)
e is 5
loop while not converged:
e is e * 0.5
print of (e < 0.001)
30
x
3
20
1
In Python you would write the convergence loop with an explicit threshold; in EigenScript the loop condition is the semantic intent:
# Python: the epsilon is yours to pick, plumb, and tune
while abs(e_prev - e) > 1e-9:
e_prev, e = e, step(e)loop while not converged: # EigenScript: the runtime watches e
e is step of e
And because the measurement is already paid, a trajectory is a first-class
thing you can hand to another function — trajectory of x snapshots the
observer's windows into a plain dict, and classify reads it on the other
side of the call (no equivalent exists elsewhere; a debugger watch window
is the closest analogue, and it can't cross a function boundary either):
define diagnose(t) as:
return classify of t
r is 1.0
i is 0
loop while i < 40:
r is r * 2.0
i is i + 1
print of (diagnose of (trajectory of r))
diverging
Transformations you will apply constantly when porting Python code:
| Python | EigenScript | why |
|---|---|---|
x = v |
x is v |
assignment keyword |
f(a) |
f of a |
application keyword |
f(a, b) |
f of [a, b] |
bare literal list = argument list |
f([a]) (pass a 1-element list) |
f of ([a]) |
parentheses = one argument |
True / False / None |
1 / 0 / null |
no boolean type |
x ** y |
pow of [x, y] |
^ is XOR |
len(x) |
len of x |
builtin, same name |
xs.append(v) |
append of [xs, v] |
builtins, not methods |
while c: |
loop while c: |
loop keyword |
def f(): body using nonlocal |
plain is |
assignment is outward-mutable |
d["k"] / d.get("k") |
d.k or d["k"] |
dot works on any dict |
A worked port — Python before:
def word_lengths(words):
out = {}
for w in words:
out[w] = len(w)
return out
print(word_lengths(["ada", "grace"]))EigenScript after:
define word_lengths(words) as:
out is {}
for w in words:
out[w] is len of w
return out
print of (word_lengths of (["ada", "grace"]))
{"ada": 3, "grace": 5}
(Note the of (["ada", "grace"]) — parenthesised: parentheses always
make the argument a single value, so the literal list arrives whole. A
bare of ["ada", "grace"] would also work here — two arguments to a
one-parameter function pack back into a list — but the parenthesised
form says "one list" directly and works for any arity.)