Language overview
This page describes the building blocks of handl: values, expressions, functions, and modules.
Stale relative to ADR-0106. This page describes the field/expression surface as the runtime currently evaluates it, including
(x) -> ...function forms and name-binding scope. The normative kernel design (ADR-0106: the object/arrow kernel) removes lambda-style closures, lexical environments, and mutable binding: the surface is being aligned so that functions are arrows (A ~> B), parameter lists are product projections, and types carry nominal identity viasym()rather than being predicates.
Values
handl has six value types, identical to JSON:
string— Unicode text, e.g."hello"number— integers and floating point, e.g.42or3.14boolean—trueorfalsenullarray— ordered lists, e.g.[1, 2, 3]object— key/value maps, e.g.{"name": "handl"}
Expressions
An expression evaluates to a value. The simplest expression is a literal. More complex expressions call functions:
result: type.number = math.abs(-42);
resultThis expression evaluates to 42.
Functions
Functions are always called in module.function form:
item: type.string = array.at(["a", "b", "c"], 1);
itemThe first argument is the receiver (often the value being operated on), followed by additional arguments. Conventionally, array and string utilities take the value first.
Modules
Modules group related functions. Examples:
array— array utilities (at,chunk,compact,concat, ...)str— string utilities (trim,upper,split, ...)math— numeric utilities (abs,sum,avg, ...)crypto— hashing (sha256,md5)fs— filesystem operations (read,write,exists)
The runtime decides which modules are available and how they are backed. For example, fs may be backed by std::fs on a server and by an in-memory stub in tests.
Scoping
A name binding name = value is available in the rest of the expression:
numbers: (a) -> array.every(a, type.number) = [1, 2, 3];
doubled: (a) -> array.every(a, type.number) = array.map(numbers, (n: type.number) -> n * 2);
doubledThe final expression is the result of the script.
Types as values
A type is a predicate: an ordinary handl value that you can apply to another value to get a type.bool.
Person: type.object = { name: type.string, age: type.number };
alice: Person = { name: "Alice", age: 30 };
ok: type.bool = alice is Person;
okis is type application: x is T means T(x). Record types are just objects of predicates, and array element types are predicates written with array.every.
Refinement types
A refinement type is any function with typed parameters that returns type.bool. Because types are values, you can write your own predicates and use them after : or is:
Even = (n: type.number) -> n % 2 == 0;
n: Even = 4;
nPositive = (n: type.number) -> n > 0;Adult = (p: type.object) -> p.age is type.number and p.age >= 18;Refinements can take parameters, producing a type family:
DivisibleBy = (k: type.number) -> (n: type.number) -> n % k == 0;
m: DivisibleBy(3) = 9;
mFunction return types are written after the parameter list:
addOne = (n: type.number): type.number -> n + 1;