Skip to main content

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.