Standard Library

hica’s standard library has two layers:

I/O & Display

Function Signature Description
println(s) (a) -> () Print a value to stdout with a newline
eprintln(s) (a) -> () Print a value to stderr with a newline
show(n) (a) -> string Convert a value to its string representation
show_fixed(value, n) (float, int) -> string Format a float with n decimal places

Environment

Function Signature Description
get_args() () -> list<string> Command-line arguments (excluding the program name)
get_env(key) (string) -> maybe<string> Look up an environment variable; returns Some(value) or None
set_env(key, value) (string, string) -> () Set an environment variable (overwrites existing)
exit(code) (int) -> () Exit the process with the given exit code

Environment Helpers (std/env, import "std/env" required)

Function Signature Description
env_or(key, default) (string, string) -> string Look up env var; return default if missing
env_require(key) (string) -> string Look up env var; print error and return "" if missing
env_int(key) (string) -> maybe<int> Look up env var and parse as int; None if missing or non-numeric
import "std/env"

fun main() {
  let host = env_or("HOST", "localhost")
  let port = env_int("PORT")             // maybe<int>
  println("Connecting to {host}")
  match port {
    Some(p) => println("port: {p}"),
    None => println("port not set")
  }
}

.env File Support (std/dotenv, import "std/dotenv" required)

Reads .env files and loads key=value pairs into the process environment.

Function Signature Description
dotenv_load(path) (string) -> () Load a .env file; skip vars already set in environment
dotenv_load_override(path) (string) -> () Load a .env file; overwrite existing environment variables
dotenv_parse(content) (string) -> list<(string, string)> Parse .env format string into key/value pairs
dotenv_apply(pairs) (list<(string, string)>) -> () Set env vars from a list; skip already-set keys
dotenv_apply_override(pairs) (list<(string, string)>) -> () Set env vars from a list; overwrite existing keys

Supported .env format:

import "std/dotenv"
import "std/env"

fun main() {
  dotenv_load(".env")             // load .env; don't overwrite existing vars
  let db = env_or("DB_URL", "sqlite://dev.db")
  let port = env_or("PORT", "3000")
  println("DB: {db}")
  println("PORT: {port}")
}

File I/O

Function Signature Description
read_file(path) (string) -> result<string, string> Read entire file; returns Ok(content) or Err(message)
write_file(path, content) (string, string) -> () Write a string to a file (throws on error)

Maybe Combinators

Function Signature Description
map_maybe(m, f) (maybe<a>, (a) -> b) -> maybe<b> Transform the value inside a Some; pass None through
and_then(m, f) (maybe<a>, (a) -> maybe<b>) -> maybe<b> Chain a function that returns maybe; short-circuits on None
unwrap_maybe(m) (maybe<a>) -> a Extract the Some value, or throw on None
unwrap_maybe_or(m, default) (maybe<a>, a) -> a Extract the Some value, or return default
is_some(m) (maybe<a>) -> bool True if Some
is_none(m) (maybe<a>) -> bool True if None
fun main() {
  // Transform a maybe value
  let doubled = Some(5) |> map_maybe((x) => x * 2)
  println(doubled)                // Some(10)

  // Chain maybe-returning functions
  let parsed = Some("42") |> and_then((s) => parse_int(s))
  println(parsed)                 // Some(42)

  // Safe extraction
  println(unwrap_maybe_or(None, 0))   // 0
  println(is_some(Some(1)))           // true
}

Result Combinators

Function Signature Description
unwrap(r) (result<a, b>) -> a Extract the Ok value, or throw on Err
unwrap_or(r, default) (result<a, b>, a) -> a Extract the Ok value, or return default
map_result(r, f) (result<a, b>, (a) -> c) -> result<c, b> Transform the Ok value; pass Err through
map_err(r, f) (result<a, b>, (b) -> c) -> result<a, c> Transform the Err value; pass Ok through
and_then_result(r, f) (result<a, b>, (a) -> result<c, b>) -> result<c, b> Chain a function that returns result; short-circuits on Err
is_ok(r) (result<a, b>) -> bool True if Ok
is_err(r) (result<a, b>) -> bool True if Err

File Helpers (std/io, import "std/io" required)

Written in hica itself:

Function Signature Description
read_lines(path) (string) -> list<string> Read a file and split it into lines (throws on error)
write_lines(path, lines) (string, list<string>) -> () Join lines with newlines and write to a file
fun main() {
  // Write and read back
  write_file("greeting.txt", "hello, world!\n")
  let content = read_file("greeting.txt") |> unwrap
  println(content)

  // Line-oriented I/O (throws on error)
  write_lines("names.txt", ["Kalle", "Olle", "Lisa"])
  let names = read_lines("names.txt")
  for name in names {
    println("Hi, {name}!")
  }

  // Handle errors explicitly
  match read_file("missing.txt") {
    Ok(text) => println(text),
    Err(msg) => println("Could not read: {msg}")
  }

  // Provide a fallback
  let data = read_file("config.txt") |> unwrap_or("default")
  println(data)
}

List Operations

Function Signature Description
map(xs, f) (list<a>, (a) -> b) -> list<b> Apply f to each element
filter(xs, f) (list<a>, (a) -> bool) -> list<a> Keep elements where f returns true
fold(xs, init, f) (list<a>, b, (b, a) -> b) -> b Reduce a list to a single value
length(xs) (list<a>) -> int Length of a list (also works on strings: length(s))
reverse(xs) (list<a>) -> list<a> Reverse a list
take(xs, n) (list<a>, int) -> list<a> Take the first n elements
drop(xs, n) (list<a>, int) -> list<a> Drop the first n elements
zip(xs, ys) (list<a>, list<b>) -> list<(a, b)> Pair up elements from two lists
concat(xss) (list<list<a>>) -> list<a> Flatten a list of lists
any(xs, f) (list<a>, (a) -> bool) -> bool True if f holds for any element
all(xs, f) (list<a>, (a) -> bool) -> bool True if f holds for all elements
foreach(xs, f) (list<a>, (a) -> ()) -> () Call f on each element for side effects
enumerate(xs) (list<a>) -> list<(int, a)> Pair each element with its index
find(xs, f) (list<a>, (a) -> bool) -> maybe<a> First element where f returns true, or None
head(xs) (list<a>) -> maybe<a> First element, or None if empty
tail(xs) (list<a>) -> list<a> All elements except the first
last(xs) (list<a>) -> maybe<a> Last element, or None if empty
flat_map(xs, f) (list<a>, (a) -> list<b>) -> list<b> Map then flatten
sort_by(xs, cmp) (list<a>, (a, a) -> bool) -> list<a> Sort using a comparison; cmp(a, b) returns true if a comes first

List Helpers (std/list, import "std/list" required)

Written in hica itself:

Function Signature Description
range(lo, hi) (int, int) -> list<int> Integers from lo up to (not including) hi — Python-style half-open range
range_inc(lo, hi) (int, int) -> list<int> Integers from lo to hi inclusive
sum(xs) (list<int>) -> int Sum all elements
product(xs) (list<int>) -> int Multiply all elements
unique(xs) (list<int>) -> list<int> Remove duplicates (keeps first occurrence)
intersperse(xs, sep) (list<a>, a) -> list<a> Insert sep between every element
zip_with(xs, ys, f) (list<a>, list<b>, (a, b) -> c) -> list<c> Zip and transform in one step
scan(xs, init, f) (list<a>, b, (b, a) -> b) -> list<b> Like fold but keeps all intermediate results
chunks(xs, n) (list<a>, int) -> list<list<a>> Split into groups of n
head_or(xs, default) (list<a>, a) -> a First element or default if the list is empty
take_while(xs, pred) (list<a>, (a) -> bool) -> list<a> Take elements from the front while predicate holds
drop_while(xs, pred) (list<a>, (a) -> bool) -> list<a> Drop elements from the front while predicate holds
count(xs, pred) (list<a>, (a) -> bool) -> int Count elements matching the predicate
group_by(xs, f) (list<a>, (a) -> string) -> list<(string, list<a>)> Group elements by a string key function; preserves insertion order
min_by(xs, f) (list<a>, (a) -> int) -> maybe<a> Element with the smallest int key; None on empty list
max_by(xs, f) (list<a>, (a) -> int) -> maybe<a> Element with the largest int key; None on empty list
fun main() {
  println(range(0, 5))                      // [0, 1, 2, 3, 4]
  println(range_inc(1, 5))                   // [1, 2, 3, 4, 5]
  println(sum([1, 2, 3, 4, 5]))           // 15
  println(sort_by([3, 1, 4], (a, b) => a <= b))  // [1, 3, 4]
  println(unique([1, 2, 3, 2, 1]))        // [1, 2, 3]
  println(chunks([1, 2, 3, 4, 5], 2))     // [[1, 2], [3, 4], [5]]
  println(head([10, 20, 30]))             // Some(10)
  println(last([10, 20, 30]))             // Some(30)
  println(flat_map([1, 2, 3], (x) => [x, x * 10]))  // [1, 10, 2, 20, 3, 30]
  println(head_or([], 0))                 // 0
  println(take_while([1,2,3,4,5], (n) => n < 4))  // [1, 2, 3]
  println(drop_while([1,2,3,4,5], (n) => n < 4))  // [4, 5]
  println(count([1,2,3,4,5], (n) => n % 2 == 0))  // 2
  let gs = group_by(["cat","cup","dog"], (w) => w[0:1])
  println(length(gs))                     // 2 groups: "c", "d"
  let words = ["hi", "hello", "hey"]
  match min_by(words, (s) => str_length(s)) {
    Some(w) => println(w),               // hi
    None => println("empty")
  }
}

Lazy Streams (std/stream, import "std/stream" required)

Lazy streams compose transformation steps into a single traversal pass, avoiding intermediate list allocations and enabling early termination (stopping the generator as soon as required conditions are met).

A Stream<a> is represented as a thunk-linked list structure. Lazy transformations (like filter, map, etc.) wrap and delay evaluation until a terminator (such as collect, fold, or foreach) is called to run the pipeline.

Entry Point & Transformations

Function Signature Description
stream(xs) (list<a>) -> stream_t<a> Create a lazy stream from an eager list
filter(s, pred) (stream_t<a>, (a) -> bool) -> stream_t<a> Keep only elements where the predicate returns true
map(s, f) (stream_t<a>, (a) -> b) -> stream_t<b> Transform each element using function f
flat_map(s, f) (stream_t<a>, (a) -> stream_t<b>) -> stream_t<b> Map each element to a stream, then flatten into a single stream
take(s, n) (stream_t<a>, int) -> stream_t<a> Limit the stream to the first n elements; stops the generator early
take_while(s, pred) (stream_t<a>, (a) -> bool) -> stream_t<a> Yield elements while the predicate holds; terminates on the first failure
drop_while(s, pred) (stream_t<a>, (a) -> bool) -> stream_t<a> Skip elements while the predicate holds, then yield the rest
zip(s1, s2) (stream_t<a>, stream_t<b>) -> stream_t<(a, b)> Pair elements element-by-element; terminates when either stream ends
enumerate(s) (stream_t<a>) -> stream_t<(int, a)> Pair each element with its 0-based index

Terminators (Forces Stream Evaluation)

Function Signature Description
collect(s) (stream_t<a>) -> list<a> Materialise the stream back into an eager list
fold(s, init, f) (stream_t<a>, b, (b, a) -> b) -> b Reduce the stream to a single value without building any intermediate lists
foreach(s, f) (stream_t<a>, (a) -> ()) -> () Call f on each element for side effects (inherits and infers any effects like io from f)
import "std/stream"
import "std/list"

fun main() {
  // Basic pipeline with zero intermediate list allocations:
  let lazy_result = stream([1..20])
    .filter((x) => x % 2 == 0)
    .map((x) => x * x)
    .take(5)
    .collect()
  println("stream: {lazy_result}") // [4, 16, 36, 64, 100]

  // Early termination (source stops generating after finding 3 matches):
  let first_threes = stream([1..1000])
    .filter((x) => x % 7 == 0)
    .take(3)
    .collect()
  println("first 3 multiples of 7: {first_threes}") // [7, 14, 21]

  // Fold without materialising lists:
  let sum_squares = stream([1..1000])
    .filter((x) => x % 2 == 0)
    .map((x) => x * x)
    .take(10)
    .fold(0, (acc, x) => acc + x)
  println("sum: {sum_squares}") // 1540

  // Zip and Enumerate:
  let words = ["hello", "hica"]
  let indexed = stream(words).enumerate().collect()
  println(indexed) // [(0, "hello"), (1, "hica")]
}

Pipeline Transducers (std/xform, import "std/xform" required)

Transducers decouple stream processing and query pipeline transformations from the underlying data source. This allows you to build a reusable query pipeline, save it as a variable, and run it across multiple different data collections or streams using transduce.

Transducer Constructors

Constructors beginning with xf_ are used to define and chain query transformations:

Constructor Signature Description
xf_filter(pred) ((a) -> bool) -> xform_t<a, a> Start a pipeline that filters elements
xf_filter_with(xf, pred) (xform_t<a, b>, (b) -> bool) -> xform_t<a, b> Chain a filtering step to an existing transducer
xf_map_start(f) ((a) -> b) -> xform_t<a, b> Start a pipeline that maps/transforms elements
xf_map(xf, f) (xform_t<a, b>, (b) -> c) -> xform_t<a, c> Chain a mapping step to an existing transducer
xf_take_start(n) (int) -> xform_t<a, a> Start a pipeline that limits stream to the first n elements
xf_take(xf, n) (xform_t<a, b>, int) -> xform_t<a, b> Chain a limit step to an existing transducer
xf_take_while_start(pred) ((a) -> bool) -> xform_t<a, a> Start a pipeline that takes elements while a condition holds
xf_take_while(xf, pred) (xform_t<a, b>, (b) -> bool) -> xform_t<a, b> Chain a take-while step to an existing transducer
xf_drop_while_start(pred) ((a) -> bool) -> xform_t<a, a> Start a pipeline that drops elements while a condition holds
xf_drop_while(xf, pred) (xform_t<a, b>, (b) -> bool) -> xform_t<a, b> Chain a drop-while step to an existing transducer
xf_flat_map_start(f) ((a) -> stream_t<b>) -> xform_t<a, b> Start a pipeline that flat-maps each element
xf_flat_map(xf, f) (xform_t<a, b>, (b) -> stream_t<c>) -> xform_t<a, c> Chain a flat-map step to an existing transducer

Application

Function Signature Description
transduce(xs, xf) (list<a>, xform_t<a, b>) -> list<b> Apply a composed transducer to a list, running in a zero-allocation pass
import "std/stream"
import "std/xform"

fun main() {
  // Build a reusable transducer pipeline (no data source is bound yet!)
  let double_evens =
    xf_filter((x) => x % 2 == 0)
    |> xf_map((x) => x * 2)
    |> xf_take(3)

  let list1 = [1..10]
  let list2 = [11..20]

  // Apply the transducer pipeline to multiple sources
  let r1 = list1 |> transduce(double_evens)
  let r2 = list2 |> transduce(double_evens)

  println("r1: {r1}") // [4, 8, 12]
  println("r2: {r2}") // [24, 28, 32]

  // Start a transducer with map
  let double_only =
    xf_map_start((x) => x * 2)
    |> xf_filter_with((x) => x > 10)

  let r3 = [1..10] |> transduce(double_only)
  println("r3: {r3}") // [12, 14, 16, 18, 20]
}

Non-Empty Lists (std/nel, import "std/nel" required)

Provides a Nel (Non-Empty List) of strings, guaranteeing at least one element exists at the type level and preventing out-of-bounds runtime errors.

Function Signature Description
nel_of(x) (string) -> Nel Create a Non-Empty List with a single element
nel_to_list(n) (Nel) -> list<string> Convert a Non-Empty List to a standard list of strings
nel_head(n) (Nel) -> string Get the first element of the non-empty list
nel_map(n, f) (Nel, (string) -> string) -> Nel Map a function over each element of the non-empty list
nel_concat(a, b) (Nel, Nel) -> Nel Concatenate two Non-Empty Lists into a single list

Error-Accumulating Validation (std/validated, import "std/validated" required)

Provides the Validated sum type, representing either a success Valid(value) or a failure Invalid(errors) containing a Nel of error messages. Unlike the result or maybe types which immediately short-circuit on the first error, Validated executes all validation checks and accumulates all failures simultaneously.

Function Signature Description
valid(x) (string) -> Validated Wrap a success value in Valid
invalid(msg) (string) -> Validated Wrap a single error message in Invalid
map_validated(v, f) (Validated, (string) -> string) -> Validated Transform the value inside Valid; pass Invalid through
zip_validated(v1, v2, f) (Validated, Validated, (string, string) -> string) -> Validated Combine two validations; if both are Valid, applies f; if any are Invalid, accumulates all errors
as_result(v) (Validated) -> result<string, list<string>> Convert to a standard result type
from_result(r) (result<string, string>) -> Validated Convert a standard result to a Validated value
import "std/nel"
import "std/validated"

fun validate_name(s: string) : Validated =>
  if str_length(s) < 3 { invalid("Name too short") } else { valid(s) }

fun validate_email(s: string) : Validated =>
  if !contains(s, "@") { invalid("Invalid email format") } else { valid(s) }

fun main() {
  // Zip/combine multiple validations to accumulate all failures:
  let outcome = zip_validated(
    validate_name("jd"),
    validate_email("invalid-email"),
    (name, email) => "{name}:{email}"
  )

  match outcome {
    Valid(info) => println("Success: {info}"),
    Invalid(errs) => {
      println("Failed with errors:")
      for err in nel_to_list(errs) {
        println("  - {err}")
      }
    }
  }
}

Map Operations

Maps use {"key": value} syntax and are represented as list<(k, v)>. Use {:} for an empty map.

Function Signature Description
map_get(m, key) (list<(k, v)>, k) -> maybe<v> Look up a key
map_set(m, key, value) (list<(k, v)>, k, v) -> list<(k, v)> Add or update a key
map_remove(m, key) (list<(k, v)>, k) -> list<(k, v)> Remove a key
map_keys(m) (list<(k, v)>) -> list<k> All keys
map_values(m) (list<(k, v)>) -> list<v> All values
map_contains_key(m, key) (list<(k, v)>, k) -> bool Check if a key exists
map_size(m) (list<(k, v)>) -> int Number of entries
fun main() {
  let scores = {"kalle": 95, "olle": 87}
  println(scores.map_get("kalle"))       // Just(95)

  let scores2 = scores.map_set("lisa", 92)
  println(scores2.map_keys())            // ["kalle", "olle", "lisa"]

  let scores3 = scores2.map_remove("olle")
  println(scores3.map_size())            // 2
}

Since maps are lists of tuples, all list operations (filter, map, fold, etc.) work on them too.

Random Numbers

Function Signature Description
random(min, max) (int, int) -> int Random integer in [min, max], both ends included
random_float() () -> float Random float in [0.0, 1.0)

Using random or random_float gives your program the ndet (non-determinism) effect.

System Clock

Function Signature Description
now_unix() () -> int Current UTC time as a Unix epoch timestamp (seconds since 1970-01-01)
now_iso() () -> string Current UTC time as an ISO 8601 string (e.g. "2026-05-26T21:30:16Z")
unix_to_iso(epoch) (int) -> string Convert a Unix epoch integer to an ISO 8601 UTC string

Backed by Koka’s std/time library. Using any of these gives your program the ndet effect.

fun main() {
  let t = now_unix()
  let s = now_iso()
  println("epoch: {t}")
  println("iso:   {s}")
  println(unix_to_iso(1716800000))   // "2024-05-27T08:53:20Z"
}

See also the epoch/duration helpers in std/datetime for working with stored timestamps.

fun main() {
  let die = random(1, 6)
  println("You rolled a {die}")
  let f = random_float()
  println(f >= 0.0 && f < 1.0)   // true
}

Bitwise Operations

Bitwise functions operate on 32-bit integers internally. Values are converted from hica’s arbitrary-precision int to int32, the operation is applied, and the result is converted back.

Function Signature Description
bit_and(a, b) (int, int) -> int Bitwise AND
bit_or(a, b) (int, int) -> int Bitwise OR
bit_xor(a, b) (int, int) -> int Bitwise XOR
bit_not(a) (int) -> int Bitwise complement (flip all bits)
bit_shl(a, n) (int, int) -> int Shift left by n bits
bit_shr(a, n) (int, int) -> int Shift right by n bits
fun main() {
  let flags = 0b1010_1100
  let masked = bit_and(flags, 0x0F)   // keep low nibble → 12
  println(masked)

  // UFCS / dot-call style
  let shifted = flags.bit_shr(4)       // → 10
  println(shifted)
}

32-bit constraint: Values are clamped to the int32 range (−2,147,483,648 to 2,147,483,647). Suitable for flags, masks, and protocol work.

Formatting

Function Signature Description
show_fixed(value, decimals) (float, int) -> string Format a float with a fixed number of decimal places
fun main() {
  println(show_fixed(3.14159, 2))  // "3.14"
  println(show_fixed(100.0 / 3.0, 1))  // "33.3"
}

User Input

Function Signature Description
input(prompt) (string) -> string Display a prompt and read a line from stdin
fun main() {
  let name = input("What is your name? ")
  println("Hello, {name}!")
}

Math (prelude/math.hc)

Always available with no import needed. Written in hica itself:

Function Signature Description
abs(n) (int) -> int Absolute value
min(a, b) (int, int) -> int Smaller of two values
max(a, b) (int, int) -> int Larger of two values
clamp(v, lo, hi) (int, int, int) -> int Constrain v to the range [lo, hi]
gcd(a, b) (int, int) -> int Greatest common divisor
lcm(a, b) (int, int) -> int Least common multiple
pow(base, exp) (int, int) -> int Integer exponentiation
sign(n) (int) -> int Returns -1, 0, or 1
isqrt(n) (int) -> int Integer square root, floor of √n

Float Math

Function Signature Description
sqrt(x) (float) -> float Square root
floor(x) (float) -> int Round down to integer
ceil(x) (float) -> int Round up to integer
round(x) (float) -> int Round to nearest integer
to_float(n) (int) -> float Convert integer to float
fun main() {
  println(sqrt(16.0))     // 4.0
  println(floor(3.7))     // 3
  println(ceil(3.2))      // 4
  println(round(3.5))     // 4
  println(to_float(42))   // 42.0
  println(pow(2, 10))     // 1024
  println(lcm(12, 18))    // 36
  println(isqrt(25))      // 5
  println(isqrt(26))      // 5
}

Char / String Conversions

Function Signature Description
chars(s) (string) -> list<char> Convert a string to a list of characters
from_chars(cs) (list<char>) -> string Convert a list of characters back to a string
chr(code) (int) -> char Construct a char from a Unicode code point
ord(c) (char) -> int Get the Unicode code point of a char
char_to_string(c) (char) -> string Convert a single char to a string
fun main() {
  let cs = chars("hello")
  println(cs)              // ['h', 'e', 'l', 'l', 'o']
  println(from_chars(cs))  // "hello"

  // Construct chars from code points
  println(chr(65))         // A
  println(ord('A'))        // 65

  // Build strings from code points
  let hi = from_chars([chr(72), chr(105)])
  println(hi)              // Hi
}

String Operations

Primitive string functions backed by Koka’s string library:

Function Signature Description
str_length(s) (string) -> int Number of characters in a string
contains(s, sub) (string, string) -> bool True if s contains sub
str_contains(s, sub) (string, string) -> bool Alias for contains
starts_with(s, pre) (string, string) -> bool True if s starts with pre
ends_with(s, suf) (string, string) -> bool True if s ends with suf
trim(s) (string) -> string Remove leading and trailing whitespace
trim_start(s) (string) -> string Remove leading whitespace
trim_end(s) (string) -> string Remove trailing whitespace
to_upper(s) (string) -> string Convert to uppercase
to_lower(s) (string) -> string Convert to lowercase
split(s, sep) (string, string) -> list<string> Split a string by separator. Empty separator splits into individual characters
replace(s, old, new) (string, string, string) -> string Replace all occurrences
join(xs, sep) (list<string>, string) -> string Join a list of strings with separator
index_of(s, sub) (string, string) -> maybe<int> Index of first occurrence of sub, or None
to_int(s) (string) -> int Parse string as integer (-1 if invalid)
parse_int(s) (string) -> maybe<int> Parse string as integer, returning None if invalid
parse_float(s) (string) -> maybe<float> Parse string as float, returning None if invalid

String Comparison

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

fun main() {
  println("apple" < "banana")   // true
  println("zoo" > "abc")        // true
  println("abc" <= "abc")       // true
}

String Indexing & Slicing

Strings support the same [] syntax as lists:

Syntax Returns Description
s[i] char Character at index i
s[i:j] string Substring from i to j (exclusive)
s[:j] string First j characters
s[i:] string From index i to end
s[-1] char Last character (negative indexing)
fun main() {
  let s = "hello"
  println(s[0])      // 'h'
  println(s[1:4])    // "ell"
  println(s[:3])     // "hel"
  println(s[-1])     // 'o'
}

String Helpers (prelude/strings.hc)

Always available with no import needed. Written in hica itself:

Function Signature Description
is_empty(s) (string) -> bool True if the string has zero length (also works on lists: is_empty(xs))
is_blank(s) (string) -> bool True if the string is empty after trimming
words(s) (string) -> list<string> Split on spaces, removing empty parts
lines(s) (string) -> list<string> Split on newlines
unwords(ws) (list<string>) -> string Join words with a space
unlines(ls) (list<string>) -> string Join lines with a newline
repeat_str(s, n) (string, int) -> string Repeat a string n times
pad_left(s, width, ch) (string, int, string) -> string Pad on the left to width
pad_right(s, width, ch) (string, int, string) -> string Pad on the right to width
center(s, width, ch) (string, int, string) -> string Center s in width, padding with ch
removeprefix(s, pre) (string, string) -> string Remove prefix if present

Trust Boundaries (std/trusted, import "std/trusted" required)

Provides the Trusted type: a string that has explicitly crossed a validation boundary. The constructor is private to std/trusted, so the only way to obtain a Trusted value is through one of the validators or the explicit trust() escape hatch. Functions that require validated data declare their parameters as Trusted instead of string, making it a compile-time error to pass a raw string.

See also: opaque struct / pub struct … priv in the language reference.

Core

Function Signature Description
trust(s) (string) -> Trusted Explicit trust marker — wrap a string you have manually verified is safe
trusted_value(t) (Trusted) -> string Unwrap the inner string from a Trusted value

Validators

Each validator returns Some(Trusted) on success, None on failure.

Function Signature Description
validate_nonempty(s) (string) -> maybe<Trusted> Accept any non-empty string
validate_maxlen(s, n) (string, int) -> maybe<Trusted> Accept strings whose length does not exceed n
validate_range(s, min, max) (string, int, int) -> maybe<Trusted> Accept strings whose length is between min and max (inclusive)
validate_alnum(s) (string) -> maybe<Trusted> Accept non-empty strings containing only [A-Za-z0-9]
validate_with(s, pred) (string, (string) -> bool) -> maybe<Trusted> Accept strings that satisfy a custom predicate

Combinators

Function Signature Description
validate_and(a, b) (maybe<Trusted>, maybe<Trusted>) -> maybe<Trusted> Both validators must pass
trusted_or(t, fallback) (maybe<Trusted>, string) -> Trusted Unwrap or use fallback (the fallback is implicitly trusted)
import "std/trusted"

// Only Trusted values may reach this function — enforced at compile time.
fun audit_log(event: string, actor: Trusted) {
  println("[AUDIT] " + event + " actor=" + trusted_value(actor))
}

fun handle_request(raw_actor: string) {
  let actor = validate_and(
    validate_nonempty(raw_actor),
    validate_alnum(raw_actor)        // rejects spaces, control chars, metacharacters
  )
  match actor {
    Some(a) => audit_log("login", a),
    None    => println("rejected: invalid actor")
  }

  // This does NOT compile — raw string cannot be passed where Trusted is required:
  // audit_log("login", raw_actor)
}

Extended String Helpers (std/string, import "std/string" required)

Written in hica itself:

Function Signature Description
count_substr(s, sub) (string, string) -> int Count occurrences of sub in s
surround(s, wrap) (string, string) -> string Wrap s with wrap on both sides
capitalise(s) (string) -> string Uppercase first letter, lowercase rest
capwords(s) (string) -> string Capitalise each word
shout(s) (string) -> string Convert to uppercase and append !
removesuffix(s, suf) (string, string) -> string Remove suffix if present

Glob & Character Classification (prelude/glob.hc)

Always available with no import needed. Written in hica itself. Provides character-level classification and glob pattern matching.

Character Classification

Function Signature Description
is_digit(c) (char) -> bool True if c is 09
is_upper(c) (char) -> bool True if c is AZ
is_lower(c) (char) -> bool True if c is az
is_alpha(c) (char) -> bool True if c is a letter
is_alnum(c) (char) -> bool True if c is a letter or digit
is_space(c) (char) -> bool True if c is whitespace (space, tab, newline, carriage return)
is_punct(c) (char) -> bool True if c is punctuation (printable, non-alnum, non-space)

String-Level Helpers

Function Signature Description
all_digits(s) (string) -> bool True if every character is a digit
all_alpha(s) (string) -> bool True if every character is a letter
all_upper(s) (string) -> bool True if every character is uppercase
all_lower(s) (string) -> bool True if every character is lowercase
all_alnum(s) (string) -> bool True if every character is alphanumeric

Glob Matching

Function Signature Description
glob_match(pattern, s) (string, string) -> bool Match with * (any chars, not /) and ? (one char)
glob_match_path(pattern, path) (string, string) -> bool Path-aware matching with ** (zero or more directories)
fun main() {
  // Character classification
  println(is_digit(chr(48)))          // true ('0')
  println(is_alpha(chr(65)))          // true ('A')
  println(all_digits("12345"))        // true
  println(all_upper("HELLO"))         // true

  // Simple glob
  println(glob_match("*.txt", "readme.txt"))   // true
  println(glob_match("h?llo", "hello"))        // true

  // Path glob with **
  println(glob_match_path("src/**/*.hc", "src/lib/util.hc"))   // true
  println(glob_match_path("**/*.txt", "a/b/c/file.txt"))       // true
}

Datetime (std/datetime, import "std/datetime" required) — v0.1.0

Note: This is a string-based datetime implementation. All datetimes are represented as plain strings in ISO 8601 format. No rich datetime types or timezone database. hica supports validation, decomposition, comparison, and epoch-based duration helpers. For obtaining the current time use the built-in now_unix(), now_iso(), and unix_to_iso() primitives (no import needed).

Written in hica itself. Import with import "std/datetime". Supports the four ISO 8601 datetime variants:

Validation

Function Signature Description
is_valid_date(s) (string) -> bool Validate YYYY-MM-DD (handles leap years)
is_valid_time(s) (string) -> bool Validate HH:MM:SS or HH:MM:SS.frac
is_valid_offset(s) (string) -> bool Validate Z, +HH:MM, or -HH:MM
is_local_date(s) (string) -> bool Check local date format
is_local_time(s) (string) -> bool Check local time format
is_local_datetime(s) (string) -> bool Check YYYY-MM-DDThh:mm:ss[.frac]
is_iso_datetime(s) (string) -> bool Check offset datetime (with Z/±HH:MM)
datetime_kind(s) (string) -> string Classify: "local-time", "local-date", "local-datetime", "offset-datetime", or "invalid"

Decomposition

Function Signature Description
date_parts(s) (string) -> result<(int, int, int), string> Decompose into (year, month, day)
time_parts(s) (string) -> result<(int, int, int), string> Decompose into (hour, minute, second)
datetime_date(s) (string) -> result<string, string> Extract the date portion
datetime_time(s) (string) -> result<string, string> Extract the time portion (offset stripped)
datetime_offset(s) (string) -> maybe<string> Extract the offset, or None for local

Comparison

Function Signature Description
date_cmp(d1, d2) (string, string) -> int Returns -1, 0, or 1
time_cmp(t1, t2) (string, string) -> int Returns -1, 0, or 1
datetime_cmp(d1, d2) (string, string) -> int Compare local datetimes; returns -1, 0, or 1
is_before(d1, d2) (string, string) -> bool Works on dates, times, and local datetimes

Offset & Weekday

Function Signature Description
offset_to_minutes(s) (string) -> result<int, string> "+02:00"120, "Z"0
day_of_week(s) (string) -> result<string, string> Returns "monday" through "sunday"

Epoch & Duration

Work with Unix epoch timestamps obtained from now_unix(). No import is needed for now_unix()/now_iso()/unix_to_iso() — they are built-in.

Function Signature Description
secs_since(epoch) (int) -> int Seconds elapsed since a stored epoch
mins_since(epoch) (int) -> int Minutes elapsed since a stored epoch
hours_since(epoch) (int) -> int Hours elapsed since a stored epoch
days_since(epoch) (int) -> int Days elapsed since a stored epoch
is_older_than_days(epoch, days) (int, int) -> bool True if the epoch is more than days days ago
is_older_than_hours(epoch, hours) (int, int) -> bool True if the epoch is more than hours hours ago
secs_diff(a, b) (int, int) -> int Absolute difference in seconds between two epochs
days_diff(a, b) (int, int) -> int Absolute difference in days between two epochs

Date String → Epoch

Convert a calendar date into a Unix epoch so you can store and compare it using the duration helpers above.

Function Signature Description
date_to_unix(s) (string) -> int Parse "YYYY-MM-DD" and return the Unix epoch at midnight UTC; returns -1 for invalid or pre-1970 dates
import "std/datetime"

fun main() {
  let deadline = date_to_unix("2026-12-31")
  let release  = date_to_unix("2024-01-01")
  println(days_since(release))                    // days since release
  println(is_older_than_days(deadline, 365))      // false — still in the future
  println(days_diff(deadline, date_to_unix("2026-01-01")))  // 364
}
import "std/datetime"

fun main() {
  let created_at = now_unix() - 7300   // simulate a 2-hour-old record
  println(hours_since(created_at))           // 2
  println(is_older_than_hours(created_at, 1)) // true
  let updated_at = now_unix() - 172800
  println(days_diff(now_unix(), updated_at))  // 2
}

Internal Helpers

These are implementation details exposed as pub due to hica’s flat module model. They are available after import "std/datetime" but are not part of the stable public API:

Function Signature Description
all_digits(s) (string) -> bool True if every character is a digit
in_range(n, lo, hi) (int, int, int) -> bool Inclusive range check
days_in_month(year, month) (int, int) -> int Days in a month (handles leap years)
parse_part(s, start, len) (string, int, int) -> maybe<int> Parse substring as integer
is_valid_time_short(s) (string) -> bool Validate HH:MM (no seconds)
is_valid_time_full(s) (string) -> bool Validate HH:MM:SS[.frac]
check_z_offset(rest) (string) -> bool Validate time+Z/z suffix
check_numeric_offset(rest) (string) -> bool Validate time+±HH:MM suffix
strip_offset(rest) (string) -> string Strip offset suffix from time string
list_int_nth(xs, i) (list<int>, int) -> int Lookup by index with 0 default
import "std/datetime"

fun main() {
  println(datetime_kind("07:32:00"))                // "local-time"

  // Decompose a date
  match date_parts("2024-05-15") {
    Ok(parts) => println("{parts.0}-{parts.1}-{parts.2}"),
    Err(e) => println(e)
  }

  // Compare dates
  println(is_before("2024-01-01", "2024-12-31"))    // true

  // Offset conversion
  match offset_to_minutes("+05:30") {
    Ok(m) => println(m),   // 330
    Err(e) => println(e)
  }

  // Weekday
  match day_of_week("2026-05-15") {
    Ok(d) => println(d),   // "friday"
    Err(e) => println(e)
  }
}

Examples

Map and filter

fun main() {
  let nums = [1, 2, 3, 4, 5]
  let evens = filter(nums, (x) => x % 2 == 0)
  let doubled = map(nums, (x) => x * 2)
  println(evens)
  println(doubled)
}

Fold

fun main() {
  let nums = [1, 2, 3, 4, 5]
  let total = fold(nums, 0, (acc, x) => acc + x)
  println(total)
}

Pipe and dot-call with standard library

Both |> and dot-call syntax work with any standard library function:

fun main() {
  // Pipe style
  let a = [1, 2, 3, 4, 5]
    |> filter((x) => x % 2 == 0)
    |> map((x) => x * 10)
    |> fold(0, (acc, x) => acc + x)
  println(a)

  // Dot-call style (equivalent)
  let b = [1, 2, 3, 4, 5]
    .filter((x) => x % 2 == 0)
    .map((x) => x * 10)
    .fold(0, (acc, x) => acc + x)
  println(b)
}

String operations

fun main() {
  let msg = "  Hello, World!  "
  println(trim(msg))
  println(to_upper(trim(msg)))
  println(contains(msg, "World"))

  let csv = "kalle,olle,lisa"
  println(split(csv, ","))
  println(join(split(csv, ","), " & "))
  println(replace(csv, ",", " | "))
}

String helpers from prelude

fun main() {
  println(words("the  quick   fox"))
  println(pad_left("42", 6, "0"))
  println(surround("hello", "**"))
}