Language Reference

A comprehensive reference for hica’s syntax and semantics.

Functions

Named functions

fun add(a, b) {
  a + b
}

Expression-bodied functions (arrow syntax)

fun double(x) => x * 2

Type annotations

fun add(a: int, b: int) : int => a + b

Type annotations are optional. Hindley-Milner inference handles most cases.

Visibility

Mark a function as pub to make it public (exported from the module):

pub fun greet(name: string) : string => "Hello, " + name

Functions without pub are private to the module.

noinline

The noinline modifier tells the Koka compiler not to inline a function. This prevents aggressive inlining that can cause extremely long compile times in large parser or codegen modules:

noinline fun parse_expr(tokens: list<string>) : list<string> =>
  // ...

pub noinline fun parse(src: string) : list<string> =>
  parse_expr(src.split(" "))

Use noinline on hot recursive or mutually-recursive functions in performance-sensitive libraries where Koka’s inliner would otherwise unroll deeply. It can be combined with pub: pub noinline fun.

Lambdas / closures

let sq = (n) => n * n
let add = (a, b) => a + b

Closures capture variables from their enclosing scope:

fun make_adder(n) => (x) => x + n

fun main() {
  let add5 = make_adder(5)
  println(add5(10))
}

Recursion

Functions can call themselves (self-recursion):

fun factorial(n) => if n <= 1 { 1 } else { n * factorial(n - 1) }

Functions can also call each other (mutual recursion). The compiler detects cycles automatically. No forward declarations needed:

fun check_even(n) => if n == 0 { true } else { check_odd(n - 1) }

fun check_odd(n) => if n == 0 { false } else { check_even(n - 1) }

Variables

Variables are bound with let and are immutable:

let x = 42
let name = "Alicia"
let pi = 3.14

Integer literals support binary (0b), hexadecimal (0x), and underscore separators for readability:

let flags  = 0b1010        // binary → 10
let colour = 0xFF          // hex → 255
let big    = 1_000_000     // underscores are ignored → 1000000
let mask   = 0b1111_0000   // binary with separators → 240

Mutable variables

Use var to declare a mutable variable. Reassign it with =:

var count = 0
count = count + 1
println(count)

var is locally scoped and effect-safe: mutable variables cannot leak out of the function they’re declared in.

The last-line rule

The last expression in a { } block is its return value. No need to write “return”. Use println() to see output.

fun main() {
  let a = 10
  let b = 20
  let c = a + b
  println(c)
}

Control Flow

If / else

if/else are expressions that return values:

let sign = if x < 0 { "negative" } else { "non-negative" }

Else-if chains

fun fizzbuzz(n) =>
  if n % 15 == 0 { "fizzbuzz" }
  else if n % 3 == 0 { "fizz" }
  else if n % 5 == 0 { "buzz" }
  else { "{n}" }

Match expressions

Pattern matching with integer, string, and wildcard patterns:

fun describe(x) => match x {
  0 => "zero",
  1 => "one",
  _ => "many"
}

Match guards add conditions to patterns with if:

fun classify(n) => match n {
  x if x < 0   => "negative",
  0             => "zero",
  x if x > 100 => "big",
  _             => "small positive"
}

Guards work with all pattern types, including constructors:

match parse_int(input) {
  Some(n) if n < 0 => "negative",
  Some(n)          => "valid: {n}",
  None             => "not a number"
}

Works with Maybe and Result types:

match safe_divide(10, 3) {
  Ok(n)  => println(n),
  Err(e) => println(e)
}

match find_user(id) {
  Some(user) => println(user),
  None       => println("not found")
}

Or-patterns match multiple values in one arm with |:

fun day_type(day) => match day {
  "Saturday" | "Sunday" => "weekend",
  _                     => "weekday"
}

fun classify(n) => match n {
  1 | 2 | 3 => "low",
  4 | 5 | 6 => "mid",
  _         => "high"
}

Range patterns match a contiguous range of integers with ..= (inclusive on both ends):

fun grade(score: int) => match score {
  0..=59   => "F",
  60..=69  => "D",
  70..=79  => "C",
  80..=89  => "B",
  90..=100 => "A",
  _        => "invalid"
}

Tuple destructuring patterns:

fun describe(point) => match point {
  (0, 0) => "origin",
  (x, 0) => "on x-axis at {x}",
  (0, y) => "on y-axis at {y}",
  (x, y) => "({x}, {y})"
}

Struct destructuring patterns:

struct Point { x: int, y: int }

fun describe(p: Point) : string => match p {
  Point { x: 0, y: 0 } => "origin",
  Point { x, y: 0 }    => "on x-axis at {x}",
  Point { x: 0, y }    => "on y-axis at {y}",
  Point { x, y }       => "({x}, {y})"
}

Write just the field name (x) to bind it to a variable with that name, or field: pattern to match a specific value. Fields not mentioned in the pattern are ignored (treated as wildcards):

struct Player { name: string, score: int, level: int }

fun rank(p: Player) : string => match p {
  Player { score: 0 }      => "newcomer",
  Player { level, score }  => "level {level} with {score} pts"
}

List slice patterns destructure lists by shape. Use [] for empty, [x] for a single element, [x, y] for exactly two, and [x, ..rest] to split into head and tail:

fun describe(xs: list<int>) : string => match xs {
  []           => "empty",
  [x]          => "just {x}",
  [x, y]       => "{x} and {y}",
  [x, ..rest]  => "starts with {x}, {length(rest)} more"
}

Slice patterns make recursive list processing clean:

fun sum(xs: list<int>) : int => match xs {
  []          => 0,
  [x, ..rest] => x + sum(rest)
}

Use .. without a name to ignore the tail:

[x, ..] => "starts with {x}"

Bit patterns match integers by their binary representation using 0b literals with ? wildcards. Each ? matches either 0 or 1:

fun decode(opcode) => match opcode {
  0b1100_???? => "high nibble is C",
  0b0000_0001 => "exactly 1",
  _           => "other"
}

The ? wildcard means “don’t care”: the bit at that position is not checked. This is useful for matching bit fields in protocols, instruction encodings, or hardware registers:

fun classify_instruction(byte) => match byte {
  0b11??_???? => "category 3",
  0b10??_???? => "category 2",
  0b01??_???? => "category 1",
  0b00??_???? => "category 0"
}

Bit patterns combine with guards:

match flags {
  0b????_1??? if flags > 100 => "high bit 3 set and large",
  0b????_1??? => "bit 3 set",
  _ => "bit 3 clear"
}

Loops

For-range loops

for i in 0..10 {
  println(i)
}

For-in collection loops

let names = ["Kalle", "Olle", "Lisa"]
for name in names {
  println(name)
}

Repeat

repeat(5) {
  println("hello")
}

While loops

var x = 5
while x > 0 {
  println(x)
  x = x - 1
}

The condition must be a bool. The body runs until the condition becomes false.

Loop (infinite)

loop {
  println("running")
  if done { break }
}

Repeats forever until break is called.

Break and continue

break exits the enclosing loop. continue skips to the next iteration. Both work in all loop types: while, for, repeat, and loop.

for i in 0..10 {
  if i % 2 == 0 { continue }
  if i > 7 { break }
  println(i)
}

Data Types

Primitives

Type Example Description
int 42, -7 Integer numbers
float 3.14, -0.5 Floating-point numbers
string "hello" Text strings
char 'a', '!' Single characters (see chr, ord in Standard Library)
bool true, false Boolean values

Strings

Concatenation with + and interpolation with "{expr}":

let name = "world"
let greeting = "Hello, " + name
let msg = "2 + 2 = {2 + 2}"

Escape sequences

Use backslash to include special characters in strings:

Escape Character
\" Double quote
\\ Backslash
\n Newline
\t Tab
\{ Literal { (prevents interpolation)
\} Literal }
println("She said \"hello\"")
println("line one\nline two")
println("col1\tcol2")
println("C:\\Users\\file.txt")
println("use \{braces\} literally")

Escapes work in both plain and interpolated strings:

let name = "world"
println("hello, {name}!\nbye!")

Strings support <, >, <=, >= for lexicographic comparison:

println("apple" < "banana")    // true
println("abc" <= "abc")        // true

String utility functions are built in using hica’s prelude library:

fun main() {
  let s = "  Hello, World!  "
  println(str_length(s))
  println(trim(s))
  println(to_upper(trim(s)))
  println(contains(s, "World"))
  println(starts_with(trim(s), "Hello"))
  println(split("a,b,c", ","))
  println(join(["a", "b", "c"], "-"))
  println(replace("hello", "l", "r"))
  println(index_of("hello-world", "-"))
  println(to_int("42"))
  println(parse_int("42"))
  println(parse_float("3.14"))
}

See the Standard Library for the full list.

Tuples

let pair = (1, "hello")
let x = pair.0    // 1
let y = pair.1    // "hello"

// Destructuring
let (a, b) = (10, 20)

Structs

Named records with typed fields:

struct Point { x: int, y: int }

fun main() {
  let p = Point { x: 3, y: 4 }
  println(p.x)     // 3
  println(p.y)     // 4
  println(p)        // Point(x: 3, y: 4)
}

Structs work as function parameters and return types:

struct Point { x: int, y: int }

fun distance_sq(p: Point) : int => p.x * p.x + p.y * p.y

fun origin() : Point => Point { x: 0, y: 0 }

Struct names must start with an uppercase letter. Fields are accessed with dot notation.

Structs also auto-derive show and ==. Equality is field-wise — two struct values are equal iff every field is equal:

struct Point { x: int, y: int }

let a = Point { x: 3, y: 4 }
let b = Point { x: 3, y: 4 }
let c = Point { x: 5, y: 4 }
println(show(a == b))   // True
println(show(a == c))   // False

Struct update syntax

Create a new struct from an existing one, overriding specific fields with { ...base, field: value }:

struct Point { x: int, y: int }

fun main() {
  let p = Point { x: 3, y: 4 }
  let q = Point { ...p, x: 10 }     // Point(x: 10, y: 4)
  let r = Point { ...p }            // copy: Point(x: 3, y: 4)
}

The original value is unchanged (structs are immutable). The compiler checks that override fields exist in the struct and have the right types.

Opaque structs — type-safe boundaries

By default any module can construct a struct directly. Opaque structs lock the constructor to the defining module, forcing callers to go through a public smart constructor that can enforce invariants.

opaque struct — both the type name and the constructor are private to the defining module:

opaque struct Token { data: string }

// Only this module can build a Token:
pub fun make_token(s: string) : Token => Token { data: s }
pub fun token_str(t: Token) : string => t.data

pub struct … priv — the type name is public (usable in signatures across modules) but the constructor is private:

pub struct SqlParam priv { data: string }

// The only way to obtain a SqlParam:
pub fun param(s: string) : SqlParam => SqlParam { data: s }
pub fun param_value(p: SqlParam) : string => p.data

Attempting to construct an opaque struct from another module is a compile-time error:

error: cannot construct opaque struct 'SqlParam'
       — use its module's constructor function

Rule of thumb:

Keyword Type name visible externally Constructor visible externally
struct Foo {}
opaque struct Foo {}
pub struct Foo {}
pub struct Foo priv {}

Use opaque struct for internal handles. Use pub struct … priv when callers need to name the type in their own signatures (e.g. as function parameters) but must not be able to forge values.

For a ready-made validated-string type that works across library boundaries, see std/trusted in the standard library.

Struct destructuring in match

Use struct patterns to destructure a struct in match arms:

struct Point { x: int, y: int }

fun classify(p: Point) : string => match p {
  Point { x: 0, y: 0 } => "origin",
  Point { x, y }       => "({x}, {y})"
}

See Pattern Matching for the full syntax.

Enums (Algebraic Types)

Define a type with named variants using type:

type Color {
  Red,
  Green,
  Blue
}

Variants can carry data. Each variant specifies its own fields:

type Shape {
  Circle(radius: float),
  Rect(width: float, height: float),
  Point
}

Construct enum values like function calls (no data → bare name, with data → parenthesised arguments):

let c = Red
let s = Circle(5.0)
let r = Rect(3.0, 4.0)

Pattern match on enums to handle each variant:

fun describe(s: Shape) : string => match s {
  Circle(r)  => "circle with radius {r}",
  Rect(w, h) => "{w} x {h} rectangle",
  Point      => "a point"
}

The compiler checks exhaustiveness: if you forget a variant, you get a warning:

warning: non-exhaustive match: missing Circle(…)

Enum names and variant names must start with an uppercase letter. println auto-shows enum values (e.g. Circle(5), Red).

Enums also auto-derive ==. Two enum values are equal iff they are the same variant and every payload field is equal; payloads dispatch to their own ==, so nested user types and containers Just Work:

type Modifier { Ctrl, Alt, Meta, Shift }
type Key      { KChar(c: char), KShortcut(m: Modifier, c: char) }

println(show(Ctrl == Ctrl))                                   // True
println(show(Ctrl == Alt))                                    // False
println(show(KShortcut(Ctrl, 'q') == KShortcut(Ctrl, 'q')))    // True
println(show(KShortcut(Ctrl, 'q') == KShortcut(Alt, 'q')))     // False
println(show(KChar('x') == KShortcut(Ctrl, 'q')))              // False

Reserve match for destructuring payloads; use == for “am I this variant” checks:

fun quit_pressed(k: Key) : bool => match k {
  KShortcut(m, c) => m == Ctrl && c == 'q',   // extract payload, then compare
  KChar(_)        => false
}

Note: if x == Ctrl { ... } parses Ctrl { ... } as a struct literal because Name { is greedy in expression position. Parenthesise the condition (if (x == Ctrl) { ... }) to disambiguate.

Enum vs Struct: Use a struct when every value has the same fields (AND of fields). Use an enum when a value can be one of several alternatives (OR of shapes).

Lists

Homogeneous, immutable lists:

let nums = [1, 2, 3, 4, 5]
let empty = []
let words = ["hello", "world"]

Maps

Key-value dictionaries using {"key": value} syntax:

let ages = {"kalle": 30, "olle": 25, "lisa": 35}
let empty = {:}

Maps are represented as lists of tuples under the hood. All list operations work on maps too.

Map functions:

Function Description
map_get(m, key) Look up a key, returns maybe<v>
map_set(m, key, value) Add or update a key
map_remove(m, key) Remove a key
map_keys(m) List of all keys
map_values(m) List of all values
map_contains_key(m, key) Check if a key exists
map_size(m) Number of entries
fun main() {
  let m = {"x": 1, "y": 2}
  println(m.map_get("x"))           // Just(1)
  let m2 = m.map_set("z", 3)
  println(m2.map_keys())            // ["x", "y", "z"]
}

Maybe

Optional values:

let x = Some(42)
let y = None

Result

Success or failure:

fun safe_divide(a, b) =>
  if b == 0 { Err("division by zero") }
  else { Ok(a / b) }

Combinators

Instead of nesting match expressions, use combinators to transform and chain Maybe and Result values. All are pipe-friendly (value first):

// Maybe: transform the inner value
let doubled = Some(5) |> map_maybe((x) => x * 2)       // Some(10)

// Maybe: chain functions that return Maybe
let parsed = Some("42") |> and_then((s) => parse_int(s))  // Some(42)

// Result: transform the Ok value
let r = safe_divide(10, 2) |> map_result((n) => n * 10)   // Ok(50)

// Result: chain fallible operations
let r2 = safe_divide(10, 2)
  |> and_then_result((n) => safe_divide(n, 1))             // Ok(5)

See the Standard Library for the full list of combinators.

Lazy Streams

Lazy streams (via std/stream) combine sequence transformations (such as map, filter, and take) into a single traversal pass, avoiding intermediate list allocations and stopping evaluation as soon as termination criteria are satisfied.

import "std/stream"

fun main() {
  let result = stream([1..1000])
    .filter((x) => x % 2 == 0)
    .map((x) => x * x)
    .take(5)
    .collect() // Materialise stream to eager list in one pass

  println(result) // [4, 16, 36, 64, 100]
}

Pipeline Transducers

Transducers (via std/xform) decouple transformations from the underlying data source entirely. This allows you to define a reusable query pipeline as a variable, compose it left-to-right using the |> operator, and apply it to multiple different sources.

import "std/stream"
import "std/xform"

// Define reusable, decoupled query pipeline
let process_evens =
  xf_filter((x) => x % 2 == 0)
  |> xf_map((x) => x * 2)
  |> xf_take(3)

fun main() {
  let list1 = [1..10]
  let list2 = [11..20]

  println(list1 |> transduce(process_evens)) // [4, 8, 12]
  println(list2 |> transduce(process_evens)) // [24, 28, 32]
}

User Input

Read a line from stdin with input(prompt). The prompt is printed, and the user’s response is returned as a string:

fun main() {
  let name = input("What is your name? ")
  println("Hello, " + name + "!")
}

Combine with parse_int or parse_float to read numbers:

fun main() {
  let age_str = input("How old are you? ")
  match parse_int(age_str) {
    Some(age) => println("In 10 years you'll be {age + 10}"),
    None      => println("That's not a number!")
  }
}

Random Numbers

Generate random integers with random(min, max). The result is in the range [min, max], both ends included. Use random_float() for a random float in [0.0, 1.0):

fun main() {
  let die = random(1, 6)     // 1–6
  let coin = random(0, 1)    // 0 or 1
  println("Die: {die}, Coin: {coin}")

  let f = random_float()     // e.g. 0.7342...
  println(f >= 0.0 && f < 1.0)  // true
}

Using random or random_float gives your program the ndet (non-determinism) effect, which hica check will report.

Formatting Numbers

Format floats to a fixed number of decimal places with show_fixed(value, decimals):

fun main() {
  println(show_fixed(3.14159, 2))       // "3.14"
  println(show_fixed(100.0 / 3.0, 1))   // "33.3"
}

Combine with pad_left and pad_right for aligned output:

fun main() {
  println(pad_left(show(42), 6, " "))     // "    42"
  println(pad_right("hi", 10, "."))       // "hi........"
}

See the Standard Libraryfor the full list of formatting and string helper functions.

Operators

Arithmetic

Operator Description
+ Addition
- Subtraction
* Multiplication
/ Division
% Remainder

Comparison

Operator Description
== Equal
!= Not equal
< Less than
> Greater than
<= Less than or equal
>= Greater than or equal

Comparison operators work on int, float, and string (lexicographic ordering).

Logical

Operator Description
&& Logical AND
|| Logical OR

Pipe and dot-call syntax

hica has two equivalent ways to chain function calls left to right:

fun double(x) => x * 2
fun add_one(x) => x + 1

fun main() {
  // Pipe operator: a |> f desugars to f(a)
  let a = 5 |> double |> add_one
  println(a)

  // Dot-call (UFCS): a.f() also desugars to f(a)
  let b = 5.double().add_one()
  println(b)

  // They're identical, use whichever reads better
  println(a == b)
}

Both a |> f and a.f() desugar to f(a). The pipe is compact for simple chains; dot-call reads naturally when passing extra arguments:

fun main() {
  // Dot-call with arguments: a.f(b) desugars to f(a, b)
  let nums = [1, 2, 3, 4, 5]
  let result = nums.filter((x) => x > 2).map((x) => x * 10)
  println(result)
}

Note: expr.name without parentheses is struct field access (p.x). With parentheses, expr.name(...) is a function call.

Bitwise

Bitwise operations are provided as built-in functions. They work on 32-bit integer values internally (hica’s int is converted to a 32-bit integer, the operation is applied, and the result is converted back).

Function Description
bit_and(a, b) Bitwise AND
bit_or(a, b) Bitwise OR
bit_xor(a, b) Bitwise XOR
bit_not(a) Bitwise complement (flip all bits)
bit_shl(a, n) Shift left by n bits
bit_shr(a, n) Logical shift right by n bits
fun main() {
  let flags = 255
  let masked = bit_and(flags, 15)   // keep low nibble → 15
  println(masked)

  let shifted = bit_shr(flags, 4)   // shift right 4 → 15
  println(shifted)

  let combined = bit_or(flags, 256)  // set bit 8 → 511
  println(combined)
}

With UFCS (dot-call syntax), bitwise functions chain naturally:

fun main() {
  let result = 255.bit_and(15).bit_shl(2)
  println(result)   // 60
}

32-bit constraint: Bitwise operations internally use 32-bit signed integers. Values are clamped to the int32 range (−2,147,483,648 to 2,147,483,647). This is the same behaviour as C’s int, suitable for flags, masks, and protocol work, but not for arbitrary-precision bit manipulation.

Error propagation (?)

The ? operator provides early-return propagation for both maybe<T> and result<T,E>.

With maybe<T>: if the value is Some(v), ? evaluates to v; if it is None, the enclosing function returns None immediately.

fun add_strings(a: string, b: string) : maybe<int> {
  let x = parse_int(a)?     // None → return None early
  let y = parse_int(b)?
  Some(x + y)
}

fun main() {
  println(add_strings("3", "4"))    // Some(7)
  println(add_strings("3", "abc"))  // None
}

With result<T,E>: if the value is Ok(v), ? evaluates to v; if it is Err(e), the enclosing function returns Err(e) immediately, propagating the error up the call chain.

fun read_config(path: string) : result<string, string> {
  let content = read_file(path)?   // Err → return Err early
  let trimmed = trim(content)
  Ok(trimmed)
}

fun double_parsed(s: string) : result<int, string> {
  // parse_int returns maybe<int>; convert to result before using ?
  let n = match parse_int(s) { Some(n) => Ok(n), None => Err("not a number") }?
  Ok(n * 2)
}

fun main() {
  match double_parsed("42") {
    Ok(n)  => println(n),    // 84
    Err(e) => println(e)
  }
}

Without ?, the same logic requires nesting:

fun add_strings(a: string, b: string) : maybe<int> {
  match parse_int(a) {
    None    => None,
    Some(x) => match parse_int(b) {
      None    => None,
      Some(y) => Some(x + y)
    }
  }
}

Rules:

Postfix validated try (&?)

The &? postfix operator provides syntactic sugar for unwrapping the Validated type (defined in std/validated). If the value is Valid(v), it unwraps to the success value v (of type string); if it is Invalid(errors), it performs an early return of Invalid(errors) from the enclosing function.

import "std/nel"
import "std/validated"

fun signup_sugared(uname: string, email_input: string, age_input: string) : Validated {
  let u = validate_username(uname)&?   // Invalid -> return Invalid early
  let e = validate_email(email_input)&?
  let a = validate_age(age_input)&?
  Valid("{u}:{e}:{a}")
}

Rules:

Testing

Test blocks

Define tests alongside your code using test blocks:

fun double(n: int) : int => n * 2

test "double works" {
  assert(double(3) == 6)
  assert_eq(double(0), 0)
}

test "string operations" {
  let s = "hello"
  assert(str_length(s) == 5)
  assert_eq(to_upper(s), "HELLO")
}

Run tests with hica test:

hica test my_file.hc

Assertions

Function Signature Behaviour
assert(cond) (bool) -> () Fails with “assertion failed” if cond is false
assert_eq(expected, actual) (a, a) -> () Fails with “expected X but got Y” if values differ
assert_ne(a, b) (a, a) -> () Fails with “expected values to differ” if equal
assert_true(cond) (bool) -> () Fails with “expected true but got false”
assert_false(cond) (bool) -> () Fails with “expected false but got true”
assert_contains(list, elem) (list<a>, a) -> () Fails if list does not contain element
assert_empty(list) (list<a>) -> () Fails if list is not empty
assert_not_empty(list) (list<a>) -> () Fails if list is empty

Test structure

Effects

hica supports user-defined algebraic effects via effect declarations and handle blocks. Effects let you name an abstract capability (logging, database, terminal, …), call its operations like ordinary functions, and choose at the call site how to fulfil them; the same code path runs against a real backend in production and a mock in tests.

effect Log {
  fun info(s: string)
}

fun greet(name: string) {
  info("hello, " + name)   // no IO here,  Log is abstract
}

fun main() {
  handle Log {
    info(s) => println("[LOG] " + s)
  } in {
    greet("world")
    greet("effects")
  }
}

Effect declarations live at the top level:

effect Db {
  fun query(sql: string) : int
  fun exec(sql: string)                 // return type defaults to ()
}

Handlers are expressions: handle E { arms } in { block } evaluates to the value of the block. Every operation of the effect must have exactly one arm; the checker reports missing ops, unknown-op arms, and duplicate arms with source spans.

Handler arms have their parameters typed automatically from the op signature — you never need to annotate them.

Stateful handlers (with var ...)

A handler can carry local mutable state via the with var ... clause. Each state binding is hoisted before the handler installation; every arm body and the in { ... } block share the same references. The state dies when the block returns, so each invocation gets fresh state.

effect Counter {
  fun incr()
  fun get() : int
}

fun main() {
  let n = handle Counter {
    incr() => count = count + 1,
    get()  => count
  } with var count = 0 in {
    incr(); incr(); incr()
    get()
  }
  println("counter = {show(n)}")   // counter = 3
}

Multiple state bindings share one with var clause, separated by commas: with var items = [], var size = 0. Assign to a state binding the same way you would to any var (count = count + 1); when the arm body is just an assignment, the handler splits the mutation out from the auto-resume so both the write and the () return happen.

See examples/effects/counter.hc and learn/45-effects-state.hc.

Effect rows in function types

Function type annotations can restrict which user-defined effects a callback is allowed to use. The row appears between the parameter list and the return type:

(A, B) -> <E1, E2> R

An empty row (or no row) is a wildcard and accepts any effects. A non-empty row is authoritative: the argument passed at every call site must call only ops declared in that row (plus built-in effects, console, fsys, div, etc.). Passing a callback that leaks another user-defined effect is a compile-time error at the call site.

// with_db permits only <Db> inside the callback
pub fun with_db(f: () -> <Db> int) : int {
  handle Db {
    query(sql) => 42,
    exec(sql)  => ()
  } in {
    f()
  }
}

fun list_users() : <Db> int {
  exec("UPDATE stats SET last_scan = now()")
  query("SELECT count(*) FROM users")
}

fun main() {
  let n = with_db(list_users)          // ok
  println("users = " + show(n))
}

Effect rows compare as sets: <A, B> unifies with <B, A>. See docs/effects.md for the full guide including handler nesting, the sandbox pattern, and testing recipes.

The actor keyword

actor Name { ... } is sugar over effect + spawn + ref.op(). It’s the shortest way to declare the shape of a stateful, message-driven effect:

type CounterMsg { Incr, Decr, Reset }

actor Counter {
  var count = 0

  receive(msg: CounterMsg) => match msg {
    Incr  => { }
    Decr  => { }
    Reset => { }
  }
}

fun main() {
  spawn Counter {
    send(msg) => match msg {
      Incr  => count = count + 1,
      Decr  => count = count - 1,
      Reset => count = 0
    }
  } with var count = 0 as counter

  counter.send(Incr)
  counter.send(Incr)
  counter.send(Decr)
}

Each actor declaration expands to a single item: effect Name { fun send(msg: MsgType) : () }. Users install instances with spawn Name { send(msg) => body } (with var ...)? as ref and dispatch with ref.send(msg). The var and receive body on the actor block are informational: the concrete state and behaviour live at each spawn site. Two actors declaring send(msg) never collide because dispatch is per-reference. See docs/effects.md for the full pattern and examples/effects/counter-actor.hc / examples/effects/ping-pong-actor.hc for runnable demos.

Named effects

spawn Name { arms } (with var ...)? as ident installs a fresh named-effect handler instance and binds a reference to it. Per-instance method dispatch (ref.op(args)) lets two independent handlers for the same effect coexist in one function, each with its own state.

effect Counter {
  fun incr()
  fun get() : int
}

fun main() {
  spawn Counter {
    incr() => count = count + 1,
    get() => count
  } with var count = 0 as c1

  spawn Counter {
    incr() => count = count + 1,
    get() => count
  } with var count = 100 as c2

  c1.incr(); c1.incr(); c1.incr()
  c2.incr()

  println("c1 = {show(c1.get())}")   // c1 = 3
  println("c2 = {show(c2.get())}")   // c2 = 101
}

See documentation/named-effects-design.md for the full design, examples/effects/two-counters.hc for the two-instance smoke test, examples/effects/counter-pool.hc for the pool example, and learn/48-named-effects.hc for the tutorial.

Modules & Imports

Modules

Any .hc file is a module. Mark functions with pub to make them available to other files:

// greet.hc
pub fun hello(name: string) {
  println("hello, " + name + "!")
}

pub fun goodbye(name: string) {
  println("goodbye, " + name + "!")
}

fun secret() {
  println("this is private")
}

Only pub items are visible to importers. Functions without pub stay private to their file.

Import

Use import to bring all pub items from another file into scope:

import "greet"

fun main() {
  hello("world")     // works: hello is pub
  goodbye("world")   // works: goodbye is pub
  // secret()        // error: secret is not pub
}

The path is relative to the importing file, without the .hc extension:

Selective import

Use from ... import { ... } to import only specific names:

from "greet" import { hello }

fun main() {
  hello("world")     // works: explicitly imported
  // goodbye("world")  // error: not imported
}

This is useful when a module exports many items but you only need a few, or when you want to make it clear where a name comes from.

Re-exporting with pub import

Prefix import with pub to re-export the imported items to your own importers:

// prelude.hc
pub import "math_helpers"
pub import "string_helpers"

Anyone who imports prelude gets the pub items from both math_helpers and string_helpers. This is useful for building library packages.

Import resolution rules