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 { ... }parsesCtrl { ... }as a struct literal becauseName {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.
- Entry point:
stream(xs)converts a list to a lazy stream thunk-linked list structure. - Lazy operations:
.map(),.filter(),.take(),.take_while(),.drop_while(),.zip(),.enumerate(). These return a stream immediately without traversing or evaluating. - Terminators (Eager):
.collect(),.fold(),.foreach(). These force evaluation and return the final list, reduced value, or perform side effects.
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.
- Constructors: Start with
xf_(e.g.xf_filter(pred),xf_map_start(f),xf_map(xf, f),xf_take(xf, n)). - Application: Use
transduce(list, xform)to run a transducer pipeline on a list in a single, zero-allocation pass.
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:
- The expression before
?must be of typemaybe<T>orresult<T,E>. - The enclosing function’s return type annotation is required —
?forces an early return and the compiler must know the return type to emit it correctly. Without an annotation, inference may fail. - The enclosing function must return the same wrapper type:
maybe<...>inside amaybe-returning function,result<...,E>inside aresult-returning function. You cannot use?on amaybevalue inside a function that returnsresult, or vice versa. ?cannot be used inmain()—main()returns(), which is neithermaybenorresult. Move fallible logic into a helper function and call it frommain()with amatch.?is a postfix operator and binds tighter than binary operators, soparse_int(a)? + parse_int(b)?works as expected.
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:
- The expression before
&?must be of typeValidated. - The enclosing function’s return type annotation is required —
&?forces an early return and the compiler must know the return type to emit it correctly. - The enclosing function must return the
Validatedtype. &?cannot be used inmain()—main()returns(). Move the fallible validation logic into a helper function and call it frommain()withmatch.
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
- Tests are declared at the top level (alongside functions and structs)
- Each test has a string name and a block body
- Tests can call any function defined in the same file
- No imports needed.
assertandassert_eqare built-in - Exit code is 0 on success, 1 on failure
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 ()
}
- Effect names are
PascalCase, op namessnake_case. - Op parameters are typed; the return type defaults to
()when omitted.
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:
import "greet"→ looks forgreet.hcin the same directoryimport "lib/utils"→ looks forlib/utils.hcrelative to the importing file
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
- Imports are resolved relative to the importing file, not the working directory
- Circular imports are detected and reported as errors
- Each imported file is compiled to its own Koka module, so names do not collide across files
- The import graph is processed before the main file, so imported functions are available throughout your code