1. What Hanki is
Hanki is a statically-typed language with Ruby/Crystal-flavored syntax, Erlang-style actors, and a typed effect-marker system. Pure functions and actions share a single keyword (def); the ! suffix on the name is what distinguishes the two. Hot reload at actor boundaries is a primary feature.
The toolchain has two tiers, built from one front end: a bytecode interpreter with a REPL (hanki run), and an ahead-of-time LLVM compiler (hanki build). Both are equally supported production modes.
use io
def greet(name: string) -> string
"Hello, #{name}"
end
def main!(args: List<string>) -> () [io]
io.print!(greet("world"))
end
What is distinctive
What a reader arriving from another language is least likely to have met before. Each point is developed in the section named.
- Effects are part of the type, and the boundary enforces them. A signature that says
[io]is what letshanki run --deny iorefuse to start the program, rather than discovering the write attempt somewhere in the middle of a run. (§6) - Failure is either a value or an effect, and one row carries both.
Resultwhen the caller should decide,throws Ewhen it should propagate. There is nounwrapand noexpectto reach for, so no call site can quietly promise that a failure will not happen. (§7) - Two tiers come from one front end, and parity between them is a requirement rather than an aspiration. A differential corpus compiles every example both ways and compares the output, so a divergence is a failing test and not a footnote. (§22)
- The REPL is the language, not a dynamic side door into it. A line at the prompt is type-checked and effect-checked exactly as a line in a file is, and the session is live: bindings persist, and an actor spawned at the prompt is still handling messages at the next one. The same interpreter is also the comptime and hot-reload engine, which is why the second tier pays for itself instead of costing twice. (§15, §16, §22)
- Testing is part of the language, not a library on top of it. Tests, doctests, property tests, fuzzing, and coverage all come from the compiler, so a doc example that stops compiling is a build failure. (§20)
The three layers
Hanki has one effect system - the effect row […] carried by a function's type (§6) - and three layers of code built on it:
- Pure functions -
def foo, no!. No effects; cannot call an action; cannot crash. (Totality here means no effects and no crash - termination is not checked, so a puredefmay still loop forever.) The effect-free base. - Actions -
def foo!. Synchronous effectful computation: the!authorises effects and the row says which ([io],[throws E], …). Actions are the vocabulary of effects - every side effect a program performs is performed by an action, on the caller's own stack, returning a value directly. - Actors -
actor Foo(§15). Concurrent, share-nothing entities with privatestateand a mailbox. An actor is built from actions: everyonhandler is an action (it carries an effect row, implicitly[state]), and only an action canspawnan actor or send it a message.
The layers nest: pure code knows nothing of effects; actions add effects but stay synchronous and stack-based; actors add concurrency and isolation on top of actions. main! is itself an action - the program's root actor - so every effect in a running program happens within some actor.
Actions and actors stay separate constructs because they do different things. Performing an effect is a synchronous call: it runs on the caller's own stack and returns a value. An actor is a spawned entity with its own identity, mailbox, thread, and deep-copied messages. The effect row is the single concept that unifies all three layers.