essenceDocumentation
essenceDocs
on this pageA pricing rule with its test
Coming from JavaScript?

What Essence is

What the language refuses to do, what it does instead, and where it sits beside Node or Bun.

Essence is a small compiled language for the pure part of a program: the rules, the arithmetic and the data they work on. You write the rules in Essence and call them from TypeScript. Numbers are exact, a forgotten case is a compile error, and the tests sit beside the code. A program compiles to one self-contained ES module that runs on Node, on Bun and in the browser.

A pricing rule with its test

An order is priced, or refused with a reason. The test sits in the same file as the code. Save the program as Pricing.es:

implementation {
	choice Zone {
		Domestic,
		Overseas,
	}

	type Order = {
		item: String,
		quantity: Integer,
		zone: Zone,
		discount: Optional<Integer>,
	}

	function shipping(to zone: Zone) -> Integer {
		<- match zone -> Integer {
			case #Domestic { <- 490 }
			case #Overseas { <- 2_490 }
		}
	}

	function total(of order: Order, price: Integer) -> Result<Integer, String> {
		if order.quantity::isLessThan(1) {
			<- #Failure("{order.item}: the quantity must be at least 1")
		}

		constant goods    = price::multiply(with order.quantity)
		constant discount = order.discount::value(defaultingTo 0)

		<- #Value(goods::subtract(discount)::add(shipping(to order.zone)))
	}

	constant beans: Order = {
		item = "Beans",
		quantity = 2,
		zone = #Overseas,
		discount = #Empty,
	}

	Terminal.print(total(of beans, price 1_290)) § Value(5070)
	Terminal.print(total(of { beans with quantity = 0 }, price 1_290)) § Failure("Beans: the quantity must be at least 1")
}

tests {
	test "prices an order with its shipping" {
		require #Value(cents) = total(of beans, price 1_290)

		expect cents::is(5_070)
	}
}

essence run Pricing.es compiles the file and runs it, and the program prints the two lines in the § comments. Installation puts the essence command on your machine. essence test Pricing.es runs the program’s statements, then the test:

Value(5070)
Failure("Beans: the quantity must be at least 1")

 Pricing.es
   ✓ prices an order with its shipping

 1 passed  ·  0.47 ms

Compared with JavaScript. The same rule as a TypeScript function:

type Zone = "Domestic" | "Overseas"
type Order = { item: string; quantity: number; zone: Zone; discount: number | null }

function shipping(zone: Zone): number {
	return zone === "Domestic" ? 490 : 2490
}

function total(order: Order, price: number): number {
	if (order.quantity < 1) {
		throw new Error(`${order.item}: the quantity must be at least 1`)
	}

	const goods = price * order.quantity
	const discount = order.discount ?? 0

	return goods - discount + shipping(order.zone)
}

const beans: Order = { item: "Beans", quantity: 2, zone: "Overseas", discount: null }

console.log(total(beans, 1290)) // 5070

try {
	console.log(total({ ...beans, quantity: 0 }, 1290))
} catch (error) {
	console.log((error as Error).message) // Beans: the quantity must be at least 1
}

Three things in it do not exist in Essence. Pricing.es uses an exact Integer for the number, an Optional for the null, and a Result the caller takes apart for the throw. The ternary also prices any new zone as Overseas, where Essence refuses to compile shipping until it handles the new zone.

Your first program builds a smaller program one step at a time. Each construct in this one has its own page:

  • Programs and values: implementation { … } around the statements, constant with its : Order annotation, § comments, number literals such as 1_290, and strings with a {order.item} hole. Terminal.print writes a value and a newline.
  • Method calls: :: calls a method on a value, as in order.quantity::isLessThan(1). The argument’s label is written at the call: price::multiply(with order.quantity).
  • Conditions: the if that refuses a quantity below one.
  • Functions: function total(of order: Order, price: Integer) -> … with its labelled parameters, the call total(of beans, price 1_290), and <-, which returns a value from the function.
  • Records: the record type type Order = { … } and the literal { item = "Beans", … }, whose members take = where JavaScript writes :. . reads a member, as in order.zone, and { beans with quantity = 0 } is a copy with one member changed.
  • Choices and match: choice Zone is a type with a fixed set of named cases. If you know TypeScript’s discriminated unions, a choice is one with the tag built in. #Overseas builds a case (# marks a case, not a private field), and match zone -> Integer takes one apart. Inside a case block, <- gives the match its value.
  • Optional: discount: Optional<Integer> holds #Empty or a value, and value(defaultingTo 0) reads it with 0 for an empty one.
  • Result: #Value(…) and #Failure(…), the two cases of the Result<Integer, String> that a function which can fail returns.
  • Tests: the tests block and its test. require #Value(cents) = … takes the Result apart and fails the test if it is a failure. expect fails the test when its Boolean is false.

What Essence does not have

  • No I/O beyond Terminal, Http and Async. Terminal has seven functions, each called like Terminal.print(…): print, write, inspect, describe, readLine, readAll and ask. Http sends a request, and Async is the timer and the way to describe a computation for later; both answer work rather than blocking on it. There is no JSON, no file and no environment variable. Beside them, Randomness.entropy() reads random values from the host, the JavaScript runtime that runs the program. essence run Main.es -- alpha beta hands alpha beta to the process, and nothing in the language reads them.
  • No async, await or Promise. Work that has not run is a value instead: a Future. complete waits for what one answers with, start puts one in flight, and a function is marked by writing complete in its body rather than by a word in front of it. Asynchrony has it.
  • No exceptions. There is no throw, try or catch. A function that can fail says so in its return type, as total does with its Result.
  • No null or undefined. A value that may be absent is an Optional. The value inside cannot be used until the program says what happens when it is empty.
  • No mutation of data. A record, a list or a string never changes. { order with quantity = 3 } is a new record, and order keeps its old value. There is no assignment to a member. A constant cannot be reassigned, and a variable can only be pointed at another value.
  • No operators. +, ==, &&, !, < and ?: are not syntax. Arithmetic, comparison and logic are method calls: price::add(tax), a::is(b), a::and(b), a::negate(), a::isLessThan(b). is compares records and lists by value, where === compares objects by reference. A value picked by a condition is a define { … } or a match.
  • No classes. There is no class, no this, no prototype, and no type inherits from another. Data you declare is a record or a choice. A namespace attaches methods to a type from outside, and a protocol names the methods several types share.
  • No for or while. A list is transformed with map, reduce and the other list methods. Other repetition is recursion or loop, one function whose labels (while, through, upTo) pick the kind of loop. Iteration has all of them.
  • No recursive types. A type or a choice cannot name itself, so a tree or a linked list cannot be declared. The compiler reports recursive-type-declaration, or recursive-generic-choice for a generic choice.
  • No tuples. (1, 2) does not parse. A group of values is a record with named members.
  • No packages. An import names a relative .es path such as "./Money.es", and nothing is searched for. The standard library is in scope without an import.
  • No calling JavaScript from Essence. The boundary runs one way: JavaScript calls Essence, and an Essence file cannot import JavaScript. The only host-written JavaScript that runs during a call is a function passed in as an argument.
  • No floating point. There is no floating-point number and no NaN. Essence’s own Number is the union of its exact number types.
  • No truthiness. An if condition is a Boolean. if 1 is refused as condition-not-boolean.
  • No default export. An export { … } block lists names, and a JavaScript host imports each one by its name.
  • No type arguments at a call site. identity<Integer>(1) does not parse. A generic function’s type parameters are inferred from its arguments, as Generics shows.

What Essence does instead

  • Exact numbers. An Integer has no upper bound and a Rational is an exact fraction: 0.1::add(0.2)::is(0.3) is true, and 0.1::add(0.2) prints 3/10. Numbers has every number type.
  • Optional and Result. Absence and failure are ordinary values with two cases each. Coming from JavaScript maps null, undefined, throw and try onto them.
  • A closed choice with an exhaustive match. A choice lists its cases once. A match that leaves one out is refused as missing-case, and its Note names the case it lacks: Unhandled: 'Zone#Overseas'.
  • Checked refinements. NonEmptyString and PositiveInteger are types with a check attached. Inside if name::hasCharacters(), a String is a NonEmptyString. Outside it, the compiler refuses a plain String, "" included, where a NonEmptyString is asked for. Checked refinements has the rest.
  • Tests in the same file. A failed expect labels the values that went into it. For a double that multiplies by three, expect double(2)::is(4) shows that double(2) was 6. Your first test reads a failure.
  • A checked JavaScript boundary. A value whose shape or base type does not fit is refused before the function runs. The error names the member that did not fit, as in argument 1 → .lines[0].quantity: expected Integer, got the string "two". Calling Essence from JavaScript has every rule.

Essence inside a JavaScript program

Essence is the pure core of a program, and the JavaScript around it is the shell. The shell (Node, Bun or a browser page) reads the request, the file or the clock. It hands plain JavaScript values to an Essence function and gets plain values back. An Integer comes back as a bigint, and an Optional as its value or undefined. A case of a choice with no payloads anywhere comes back as its name, such as "Overseas". Any other case, a Result included, comes back as an object with a $case.

There are three ways to call a .es file from JavaScript, and the guide Calling Essence from JavaScript walks through each:

  • loadModule from @essence-lang/client compiles a file while the host runs. It returns a promise of a module whose exports are JavaScript functions and values.
  • The Vite, esbuild and Bun plugins turn an import of a .es file into an ordinary import, and can write a .d.es.ts file for TypeScript.
  • essence build --embed writes a bundle and a descriptor of its boundary, and loadPrebuilt loads the pair without compiling anything.

examples/quote-server, listed below, uses the Bun plugin. Its server.ts parses the request and writes the response, and the pricing rules reach it through one import:

import type { Input } from "@essence-lang/client"
import { catalog, type Order, quote } from "./rules/Pricing.es"

// …

let answer = quote(body as Input<Order>)

if (answer.$case === "Quote#Rejected") {
	return toJSON(answer, 422)
}

body is the parsed JSON of the request. Input<Order> is Order with every Integer widened to bigint | number, which is what a caller may pass. A body that does not fit Order makes quote throw an EssenceMarshalError before any rule runs, and server.ts responds with a 400. Nothing in server.ts checks a field by hand.

The example programs

Four programs in the repository show Essence at full size.

  • examples/league is a six-team season as a pure program. The matches are data, and the program works out the standings and prints them with essence run.
  • examples/quote-server is a Bun HTTP API that prices orders. Every rule is Essence, reached through the Bun plugin.
  • examples/client-2048 is the game 2048 in the browser. The board and the moves are Essence, Vite serves them through one plugin line, and undo reads a board that was never overwritten.
  • examples/site-check asks several addresses for their pages at once. The requests, the bound on how many run together and the report are Essence, and a Bun script awaits the one promise the module answers with.
esc