A Tour of Epsil
In one page, write and read the Epsil programs you will use most often.
Epsil is a language for scientific computing built on the Compute Engine. Its most useful starting idea is that mathematical expressions retain their meaning: they stay exact and symbolic until you explicitly ask for an approximation.
This tour is deliberately quick. It introduces the language through complete, executable snippets and points to the guide when a feature deserves a deeper explanation.
Exact mathematics, when it matters
Ordinary arithmetic is exact. 1 / 3 is the rational number one third, not a
rounded floating-point value; symbolic expressions also remain available for
later manipulation:
let share = 1 / 3
Simplify(share + share + share)
// ➔ 1
This is valuable when a formula needs to be transformed, compared, or carried
through several steps without accumulating rounding error. Use N() at the
point where a decimal is actually useful — for presentation, plotting, or a
numerical algorithm:
N(Sqrt(2))
// ➔ 1.4142135623730951
Capitalized names such as Simplify, Sqrt, and N are Compute Engine
operators. Lowercase names are normally the names you introduce.
Names describe values
Use let for a name whose value will change, and const for one that should
not. Values themselves are immutable; let makes the binding movable.
const secondsPerMinute = 60
let elapsed = 2
elapsed = elapsed + 1
elapsed * secondsPerMinute
// ➔ 180
That distinction makes it clear which programs are stateful. A collection is never changed in place: an operation creates a new value, and you can choose whether to bind it to a new name or replace an old binding.
let readings = [3, 1, 2]
let sorted = Sort(readings)
(readings, sorted)
// ➔ ([3, 1, 2], [1, 2, 3])
Read Declarations for scopes, destructuring, and type annotations; Evaluation explains the value-and-binding model in depth.
Functions read like formulas
For a one-line mathematical definition, put parameters in parentheses and the
formula after =:
circleArea(r) = Pi * r^2
circleArea(3)
// ➔ 9π
For a function with local names or several steps, use a block. The last
expression is the result, so there is no return ceremony:
function hypotenuse(a, b) {
let squared = a^2 + b^2
Sqrt(squared)
}
hypotenuse(3, 4)
// ➔ 5
Anonymous functions use |->. They are especially useful for a small
transformation passed to a collection operator:
Map(1..5, n |-> n^2)
// ➔ [1, 4, 9, 16, 25]
Use a named function when its name explains the operation or the body needs room to grow; use a lambda when the transformation is local and obvious. More forms, including recursion and multiple clauses, are in Control Flow.
Branches produce values
if is an expression, not merely a way to choose which statements run. That
means it naturally fits in a definition or assignment:
sign(n) = "positive" if n > 0 else "not positive"
sign(-7)
// ➔ "not positive"
Choose the compact conditional when both outcomes are simple expressions. Use the block form when either branch needs local work:
function describe(n) {
if n % 2 == 0 { "even" } else { "odd" }
}
describe(42)
// ➔ "even"
The same expression-oriented style applies to match and blocks. It lets the
shape of a computation stay close to the shape of the value it produces.
Transform collections in their natural order
Lists are ordered and indexed from 1. Ranges such as 1..10 include both
endpoints. Use a pipeline when data goes through several transformations:
1..10
|> Filter(_, n |-> n % 2 == 0)
|> Map(_, n |-> n^2)
|> Sum
// ➔ 220
Pipelines read from input to result, rather than inside out. The _ marks the
argument position filled by the piped value, which matters when Map or
Filter has another argument as well.
For work whose purpose is changing a binding — an accumulator, for example — use a loop:
let total = 0
for n in 1..100 { total = total + n }
total
// ➔ 5050
Use Map, Filter, and Reduce for value-producing iteration; use for and
while when performing a sequence of updates is the clearest model.
Types document important boundaries
Epsil infers types for ordinary code, so annotations are optional. Write one where it communicates an assumption that callers must meet:
meanOfPair(a: real, b: real) -> real = (a + b) / 2
meanOfPair(2, 7)
// ➔ 9/2
Here the annotation is useful because the function models a numerical operation, not because every local calculation requires paperwork. It lets Epsil reject an unsuitable argument at the call boundary instead of leaving a surprising expression downstream.
Keep going
The Getting Started guide shows how to run Epsil in the REPL, from a file, and from JavaScript. Then choose a guide based on the problem in front of you:
When you need a precise rule rather than a guided explanation, use the Language Reference.