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) |
|
concat(List(T)) |
List(T) |
|
remove(T) |
List(T) |
|
contains(T) |
Bool |
|
len() |
Int |
|
is_empty() |
Bool |
|
sum() |
T |
where T + T is a T |
push, concat 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. concat
takes a whole list where push takes one element, and the element types have to agree
exactly.
sum exists exactly where + does, which is Int, Decimal(n) and Money(n). The
element type is the result type, an empty list is that type’s zero rather than a none, and
an overflow is an error the way it is for +.
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, an enum, or a
subject whose ids are one of those, reading through to the scalar. 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]
A comprehension writes its element expression down, so it needs no target to take a type
from: [line.amount for line in lines] is a List(Money(2)) whether or not lines has
anything in it. A target still wins where there is one, which is the coercion every declared
position does.
There is no sort, map, filter or general fold method. Iteration order is already
defined, a comprehension covers the middle two, and sum covers the one reduction.
Bindings
let and nothing else. There is no var, and a binding is never reassigned.
A let may not bind a name an enclosing block already bound. Without var an inner
let is a second binding rather than an assignment, and one written in a loop body or a
branch ends with that block:
let running = 0
for a in ns {
let running = running + a // rejected: `running` is already in scope
}
Rebinding in the scope that already holds the name is fine, because there it is sequential
and every later read sees it: let email = email.trim().lower().
A total is [line.tax for line in lines].sum(), where a comprehension selects and sum
adds. Accumulating across events is a fold arm, which is a different thing entirely.
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.
Branching
if a > 10 {
invalid "too many"
} else if a > 5 {
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 |
leave the declaration, and in a fn hand a value back |
reject / invalid |
the answer, in a command, a guard, or a fn that declared Outcome |
fail |
the answer, in an effect |
emit |
command only |
put / patch / update / delete |
projector only |
log / 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.