Types, optionals and inference
Every expression in HollowScript has a type, known before the program runs. There is no any, no implicit conversion between unrelated types, and no way to inspect a value’s type at runtime. The built-in types are Int, Float, Bool, String, Filter (an opaque, validated filter expression), lists ([T]), maps (Map<K, V>), sets (Set<T>), optionals (T?), records, enums and function types. See section 2.2 for the full list.
Types are required on function parameters, function return types, record fields and enum payloads. Everywhere else, including on constant and variable, a type is worked out for you.
Optionals
Section titled “Optionals”There is no null. A value that might be absent has an optional type, written with a trailing ?, and the only way to say “nothing here” is the value none. A record field of an absent-capable type has to say so explicitly:
uses print
record Task = { title: String, owner: String? }
fn ownerLine(task: Task): String { if task.owner == none { return "unassigned" } return "assigned to ${task.owner}"}
constant tasks: [Task] = [ { title: "Write the parser", owner: "Priya" }, { title: "Freeze the spec", owner: none }]
for t in tasks { effect print("${t.title}: ${ownerLine(t)}")}Output:
Write the parser: assigned to PriyaFreeze the spec: unassignedNotice that inside the if task.owner == none { return ... } branch’s fall-through, task.owner is used as a plain String in the return that follows, with no cast and no unwrap method. This is narrowing: the checker tracks which paths cannot be none at each point in the code, purely as a compile-time analysis with no run-time cost. Guard clauses like this one are the normal way to peel off the absent case. The full rules, including what happens across if, while, for and closures, are in section 4.8.
Inference
Section titled “Inference”constant x = e and variable x = e give x the type of e. This works for almost everything; the exceptions are the initialisers that have no type of their own to infer from, an empty list [], an empty map [:], or none on its own, which all need an annotation:
uses print
constant empty: [Int] = []
constant nothing: Int? = none
effect print("${empty.length()} ${nothing == none}")Output:
0 trueA constant or variable declaration can also destructure a record or a list into several names at once, covered together with pattern matching on the patterns page. See section 2.9 for the full inference rules, and section 2.10 for how generic functions like first<T> infer their type arguments from their arguments rather than from angle brackets at the call site, there is no first<Int>(xs) syntax at all.