essence

Goals

What Essence is for

Every language is a set of bets about what matters. These are the ones Essence makes: what it is built for, what that should make easy, and what it deliberately leaves alone.

  • Reliable, bug-free code

    The language is arranged so that the mistakes worth worrying about cannot be written: there is no null, no mutation, no implicit conversion, and no floating-point surprise. What cannot be ruled out by construction is reported before the program runs, with the span that caused it and often the fix. Reliability is the default output of writing Essence, not a discipline practiced on top of it.

  • Code that is a joy to maintain

    Most of a codebase's life is maintenance, and that is where a language is really judged. Reshaping what you wrote a year ago should be an afternoon's work rather than something you schedule: nothing shares mutable state behind your back, a type is the shape it has rather than a name you registered, and a case added to a union stops every match that does not answer for it.

  • Easy integration into existing TypeScript codebases

    Essence is meant to be able to join a codebase you already have, not demand a new one. A compiled module is one self-contained ES module with no runtime dependency, so TypeScript can import it like any other file.

  • No runtime errors

    A program that compiles is meant to have nothing left to throw. Absence is a case in a union and every match must answer for it; arithmetic is exact and has no width to overflow; failure is a value handed back, not an exception fired past you.

What should be easy

What the goals above amount to on an ordinary day: a piece of work, what should be easy about it, and what the language does to make it so. Every one of these works today.

Getting the arithmetic right

Money, ratios and measurements should add up to exactly what they should — on the first attempt, with nothing imported.

Integer and Rational are exact and arbitrary-precision, and they are the default rather than a library you had to remember. There is no floating point to fall into: a tenth is a tenth, and money comes out to the cent.

implementation {
	§ The classic floating-point trap, absent.
	Terminal.print(0.1::add(0.2)::is(0.3)) § true

	§ Money to the cent: 21 cents, three times.
	constant total = 0.19::add(0.02)::multiply(with 3)
	Terminal.print(total::toString(as #Decimal)) § "0.63"
}

Handling uncertainty

An absent value should be impossible to overlook, and dealt with where it appears — not in tomorrow's stack trace.

There is no null and nothing to dereference. Absence is the #Empty case of an Optional, and a match over one does not compile until it answers for that case — the check you would have forgotten is the one the compiler insists on.

import {
	Order        from "./receipts.es"
	emailReceipt from "./receipts.es"
	printReceipt from "./receipts.es"
}

implementation {
	§ Some orders have an address to send to, and some do not.
	function deliver(_ order: Order) -> String {
		<- match order.email -> String {
			case #Value(address) { <- emailReceipt(order, to address) }
			case #Empty          { <- printReceipt(order) }
		}
	}
}

Handling state

Changing one field deep in a structure should be one expression — and should leave the value you already had alone.

Records are structural, so a shape is a type with nothing to declare or register first. Every value is immutable, and an update names the path it changes: what comes back is a new record, and what you were holding is untouched.

implementation {
	type Filters = { search: String, page: Integer }
	type State = { filters: Filters, loading: Boolean }

	constant state: State = {
		filters = { search = "cats", page = 3 },
		loading = false,
	}

	§ An update names the path it changes.
	constant next = { state with { filters.search = "essence" } }

	Terminal.print(next.filters.search) § "essence"
	Terminal.print(state.filters.search) § "cats"
}

Changing your mind

Renaming, reshaping and rethinking should be routine work, not a rewrite you schedule.

Inference means a first draft compiles without being annotated into place. A rename reaches every file in the workspace, and the compiler names whatever stopped fitting, with the span that stopped fitting it.

Understanding what went wrong

Finding out what is wrong should take one read of the message — not a print statement, a bisect, or a search for the wording online.

Every diagnostic labels the span that caused it and shows what the compiler understood there, so the message is about your code rather than the compiler's internals. Where the repair is obvious, the editor offers to make it.

Non-goals

What Essence is not trying to be.

  • Being a superset of JavaScript

    This is its own language with its own semantics, not a way of describing JavaScript as already written. It shares an output format with TypeScript and almost nothing else.

  • Replacing a systems language

    Essence spends performance on correctness: immutable data, exact numbers by default, a garbage-collected runtime. Those are trades a systems language cannot make.

esc