Containers and control flow
Lists, maps, iteration in a defined order, and why every heklang program terminates.
Lists
let ids = [first_id, second_id]
let more = ids.push(order_id)
| Method | Returns |
|---|---|
first() |
T? |
push(T) |
List(T) |
remove(T) |
List(T) |
contains(T) |
Bool |
len() |
Int |
is_empty() |
Bool |
push and remove build a new list rather than mutating one. remove removes every
equal element, not the first, so it is idempotent the way a map’s is.
There is no indexing. first() is the only positional access, and it is optional.
Maps
| Method | Returns |
|---|---|
get(K) |
V? |
set(K, V) |
Map(K, V) |
remove(K) |
Map(K, V) |
contains(K) |
Bool |
keys() |
List(K) |
values() |
List(V) |
len() |
Int |
is_empty() |
Bool |
A map key must be a type that orders: Int, String, Uuid, Timestamp, or an enum.
That is the same set an entity key may use.
An empty container takes its type from the target. [] and Map.empty with nothing to
infer from are a compile error naming where a type could come from.
Iteration
Three forms, each over a finite container:
for item in list { }
for index, item in list { }
for key, value in map { }
Comprehensions cover map and filter:
let names = [row.name for row in rows if row.active]
There is no sort, map, filter or fold method. Iteration order is already defined, a
comprehension covers the middle two, and a fold over a container is a for inside a pure
fn.
Bindings
let and nothing else. There is no var, and a binding is never reassigned.
Immutability is what makes the rest of the guarantees cheap: a fold arm that returns new
state cannot alias the old one, a value passed to a helper cannot come back changed, and a
replay cannot diverge because something was written twice in a different order.
Accumulating is a for building a new value, or a comprehension.
Branching
if a > 10 {
return invalid("too many")
} else if a > 5 {
return reject Busy
} else {
return
}
An else if chain is one statement rather than a nest, which is why it formats flat.
if also works in value position:
let label = if paid { "paid" } else { "outstanding" }
There is no match, no switch, and no fallthrough.
Totality
Every heklang program terminates. Four rules together, and none of them is a run-time check:
- No
while. There is no unbounded loop form. - No
breakand nocontinue. Aforruns once per element of a finite container. - Recursion is rejected statically, and the diagnostic names the cycle.
- Every path must return. A
fnbody that can fall off the end is an error.
A smart contract language buys termination at run time, with gas metering and a transaction that runs out halfway. Here it is not expressible in the first place, so nothing needs to bound a runaway program: nothing can run away.
That is also why hekla does not need a timeout around your code. The only unbounded thing a hekla process does is wait on the network, and that only happens inside an effect, where it is journaled and retried.
Statements
The complete list, so it is clear how little there is:
let |
bind a value |
if / else if / else |
branch |
for |
iterate |
return |
including return invalid(...) and return reject Name |
emit |
command only |
put / patch / update / delete |
projector only |
log / fail / erase |
effect only |
a bare invoke or http.* |
effect only, when the result is unused |
a call to a void fn |
effect only, since only an effect-local fn may return nothing |
There are no semicolons anywhere in the language. Statements are separated by newlines.