Skip to content
hekla

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 break and no continue. A for runs once per element of a finite container.
  • Recursion is rejected statically, and the diagnostic names the cycle.
  • Every path must return. A fn body 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.