Hica vs Rust
hica and Rust share values like immutability, expression-oriented design, match as a core feature, and Result/Option types. They diverge in how much complexity they expose to achieve control and performance.
At a Glance
| Dimension | Rust | hica |
|---|---|---|
| Type system | Ownership + lifetimes + traits | Hindley-Milner inference (little to no annotations in practice) |
| Memory model | Borrow checker, zero-cost abstractions | Automatic reference counting with compile-time optimisation (Koka/Perceus) |
| Mutability | Immutable by default, mut opt-in |
Immutable, no mut |
| Error handling | Result<T, E> + ? operator |
Result + match + combinators + ? operator |
| Closures | Fn / FnMut / FnOnce traits |
Single closure type, always captured |
| Pattern matching | Exhaustive, deeply nested | Common cases (primitives + Maybe/Result + ranges), not deeply nested |
| Custom types | struct + enum + impl + derive |
struct + type enums (simple, no impl blocks) |
| Maps | HashMap<K, V> (mutable, hash-based) |
{"k": v} literals (immutable, list of tuples) |
| Generics | Monomorphized generics + traits | Inferred polymorphism |
| Compilation target | LLVM (native) | Koka -> C (native) |
| Loops | loop, while, for, break/continue, labeled breaks |
loop, while, for, repeat, break/continue |
| Learning curve | Steep (ownership, lifetimes, traits) | Gentle (write, run, iterate) |
| Ecosystem | Massive (crates.io) | Small, growing |
Type Inference
Rust infers local variable types but requires annotations on function signatures:
fn add(a: i32, b: i32) -> i32 {
a + b
}
let x = add(2, 3); // x inferred as i32
hica infers types across function boundaries. Annotations are optional rather than required:
fun add(a, b) => a + b
fun main() {
println(add(2, 3))
}
You can add them when you want clarity:
fun add(a: int, b: int) : int => a + b
Memory Management
This is where the languages diverge most. Rust uses ownership and borrowing, enforced at compile time:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved, no longer valid
// println!("{}", s1); // compile error: value used after move
println!("{}", s2);
}
hica has no ownership system. Koka’s Perceus reference counting handles memory automatically, so you never think about borrows, lifetimes, or moves:
fun main() {
let s1 = "hello"
let s2 = s1
println(s1)
println(s2)
}
Rust enforces memory safety at compile time through ownership. hica delegates memory management to Perceus reference counting, trading compile-time guarantees and fine-grained control for a simpler programming model.
Functions and Closures
Rust closures come in three flavors depending on how they capture:
let x = 5;
let add_x = |n| n + x; // captures by reference (Fn)
let mut v = vec![];
let push = |n| v.push(n); // captures by mutable reference (FnMut)
let name = String::from("hi");
let take = move || println!("{}", name); // captures by move (FnOnce)
hica has one closure type. Closures capture values implicitly, without exposing capture modes (no by-ref vs by-move distinction):
fun make_adder(n) => (x) => x + n
fun main() {
let add5 = make_adder(5)
println(add5(10))
println(add5(20))
}
No Fn vs FnMut vs FnOnce to reason about.
Error Handling
Both use Result types. Rust adds the ? operator for ergonomic early returns:
fn read_config() -> Result<Config, io::Error> {
let contents = fs::read_to_string("config.toml")?;
let config = parse(contents)?;
Ok(config)
}
hica uses match explicitly:
fun safe_divide(a, b) =>
if b == 0 { Err("division by zero") }
else { Ok(a / b) }
fun main() {
match safe_divide(10, 3) {
Ok(n) => println("Result: {n}"),
Err(e) => println("Error: " + e)
}
}
hica also has combinators for chaining without deeply nested match:
fun main() {
let result = safe_divide(10, 2)
|> map_result((n) => n * 10) // Ok(50)
|> and_then_result((n) => safe_divide(n, 5)) // Ok(10)
println(result)
}
For maybe types, hica also has a ? operator that works like Rust’s — unwrap or return early:
fun add_strings(a: string, b: string) : maybe<int> {
let x = parse_int(a)? // None → return None
let y = parse_int(b)?
Some(x + y)
}
hica’s ? currently works with maybe types. Rust’s ? works with both Result and Option and supports custom Try implementations.
String Operations
Rust distinguishes String (owned, heap-allocated) from &str (borrowed slice), and string methods live on these types:
let msg = " Hello, World! ";
println!("{}", msg.trim());
println!("{}", msg.to_uppercase());
println!("{}", msg.contains("World"));
let parts: Vec<&str> = "a,b,c".split(',').collect();
let joined = ["a", "b"].join(", ");
println!("{}", msg.replace("World", "Rust"));
hica has one string type and free functions:
fun main() {
let msg = " Hello, World! "
println(trim(msg))
println(to_upper(msg))
println(contains(msg, "World"))
println(split("a,b,c", ","))
println(join(["a", "b"], ", "))
println(replace(msg, "World", "hica"))
}
Indexing & slicing
| Rust | hica |
|---|---|
s.chars().nth(0).unwrap() |
s[0] |
&s[1..4] (byte indices, can panic) |
s[1:4] (char indices, safe) |
&s[..3] |
s[:3] |
s.strip_prefix("v").unwrap_or(s) |
removeprefix(s, "v") |
s.strip_suffix(".txt").unwrap_or(s) |
removesuffix(s, ".txt") |
Rust requires understanding String vs &str ownership and byte vs char indexing. hica has a single immutable string type with char-based indexing that is simpler to learn, although without Rust’s fine-grained control over allocation.
Parsing & Type Conversion
Rust uses parse() with turbofish syntax and returns Result:
let n: i32 = "42".parse().unwrap(); // 42
let bad: Result<i32, _> = "abc".parse(); // Err(...)
let f: f64 = "3.14".parse().unwrap(); // 3.14
hica uses parse_int and parse_float, returning maybe:
fun main() {
println(parse_int("42")) // Some(42)
println(parse_int("abc")) // None
println(parse_float("3.14")) // Some(3.14)
println(parse_float("xyz")) // None
match parse_int("100") {
Some(n) => println("Got: {n}"),
None => println("Not a number")
}
}
Rust’s parse() is generic over the target type and returns Result. hica uses separate named functions and returns maybe, trading generality for simplicity.
Pattern Matching
Rust has deep, exhaustive pattern matching with guards, nested destructuring, ranges, and if let:
match point {
(0, 0) => println!("origin"),
(x, 0) | (0, x) => println!("on axis: {x}"),
(x, y) if x == y => println!("diagonal"),
(x, y) => println!("({x}, {y})"),
}
match score {
0..=59 => println!("F"),
60..=69 => println!("D"),
_ => println!("C or above"),
}
hica supports integer, string, wildcard, Maybe/Result, tuple, or-patterns, range patterns, and struct destructuring:
fun describe(x) => match x {
0 => "zero",
1 | 2 | 3 => "low",
_ => "many"
}
fun grade(score: int) => match score {
0..=59 => "F",
60..=69 => "D",
90..=100 => "A",
_ => "other"
}
struct Point { x: int, y: int }
fun classify(p: Point) : string => match p {
Point { x: 0, y: 0 } => "origin",
Point { x, y } => "({x}, {y})"
}
fun sum(xs: list<int>) : int => match xs {
[] => 0,
[x, ..rest] => x + sum(rest)
}
Both languages use ..= for inclusive range patterns, support struct destructuring, and have list/slice patterns in match. Rust uses [first, rest @ ..] syntax; hica uses [first, ..rest]. Rust’s pattern matching is still more powerful (if let, @ bindings, exclusive ranges with .., nested destructuring). hica covers the common cases — including or-patterns, guards, ranges, struct patterns, and list slice patterns — with fewer constructs and edge cases to learn.
Custom Data Types
Rust uses struct with impl blocks and derive macros:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl Point {
fn distance_sq(&self) -> i32 {
self.x * self.x + self.y * self.y
}
}
fn main() {
let p = Point { x: 3, y: 4 };
println!("{}", p.distance_sq()); // 25
println!("{:?}", p); // Point { x: 3, y: 4 }
}
hica uses struct without impl blocks. Functions that operate on structs are regular free functions:
struct Point { x: int, y: int }
fun distance_sq(p: Point) : int => p.x * p.x + p.y * p.y
fun main() {
let p = Point { x: 3, y: 4 }
println(distance_sq(p)) // 25
println(p) // Point(x: 3, y: 4)
}
Rust’s impl blocks group methods on a type with self access, derive generates common trait implementations, and traits provide polymorphism. hica keeps it simple: structs hold data, free functions operate on them, and show is auto-generated. Both support struct update syntax: Rust’s Point { x: 10, ..p } is hica’s Point { ...p, x: 10 }.
Enums (Algebraic Types)
Rust has powerful enum types with impl blocks and derive:
#[derive(Debug)]
enum Shape {
Circle(f64),
Rect(f64, f64),
Point,
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect(w, h) => w * h,
Shape::Point => 0.0,
}
}
}
hica has the same core concept with a lighter syntax:
type Shape {
Circle(radius: float),
Rect(width: float, height: float),
Point
}
fun area(s: Shape) : float => match s {
Circle(r) => 3.14159 * r * r,
Rect(w, h) => w * h,
Point => 0.0
}
| Rust | hica |
|---|---|
enum Shape { Circle(f64), ... } |
type Shape { Circle(radius: float), ... } |
Shape::Circle(5.0) (qualified) |
Circle(5.0) (unqualified) |
match self { Shape::Circle(r) => ... } |
match s { Circle(r) => ... } |
#[derive(Debug)] for printing |
Auto-generated show |
impl blocks for methods |
Free functions |
Both languages have exhaustive matching — the compiler ensures every variant is handled. Rust adds generic type parameters (Option<T>, Result<T, E>), nested destructuring, and if let / let else for partial matching. hica covers the common cases with less syntax: no qualified paths (Shape::), no derive, no impl blocks.
Maps / Dictionaries
Rust uses HashMap from the standard library, requiring an import and explicit type:
use std::collections::HashMap;
fn main() {
let mut ages = HashMap::new();
ages.insert("kalle", 30);
ages.insert("olle", 25);
println!("{:?}", ages.get("kalle")); // Some(30)
ages.remove("olle");
}
hica has built-in map literals with {"key": value} syntax. Maps are immutable lists of tuples:
fun main() {
let ages = {"kalle": 30, "olle": 25}
println(ages.map_get("kalle")) // Just(30)
let ages2 = ages.map_set("lisa", 35)
let ages3 = ages2.map_remove("olle")
println(ages3.map_keys()) // ["kalle", "lisa"]
}
| Rust | hica |
|---|---|
map.get(&key) → Option<&V> |
map_get(m, key) → maybe<v> |
map.insert(key, val) (mutates) |
map_set(m, key, val) (returns new map) |
map.remove(&key) |
map_remove(m, key) |
map.keys() |
map_keys(m) |
map.values() |
map_values(m) |
map.contains_key(&key) |
map_contains_key(m, key) |
map.len() |
map_size(m) |
Rust’s HashMap is a mutable hash table with O(1) average lookup and full generic support. hica maps are immutable association lists — no imports, built-in literal syntax, and composable with all list functions (filter, map, fold), but O(n) lookup. Rust gives performance and flexibility; hica gives simplicity and immutability by default.
Immutability
Both are immutable by default. Rust lets you opt in to mutability:
let x = 5; // immutable
let mut y = 10; // mutable
y += 1;
hica has no mut. All bindings are immutable. State changes are expressed by creating new values rather than mutating existing ones:
fun main() {
let nums = [1, 2, 3]
let doubled = map(nums, (x) => x * 2)
println(doubled)
}
Pipe Operator and Dot-Call Syntax
Rust doesn’t have a built-in pipe operator. Method chaining on iterators fills a similar role:
let result: Vec<i32> = vec![1, 2, 3, 4, 5]
.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 10)
.collect();
hica has both a pipe operator |> and dot-call syntax — they’re equivalent and both work with any function:
fun main() {
// Pipe style
let a = [1, 2, 3, 4, 5]
|> filter((x) => x % 2 == 0)
|> map((x) => x * 10)
// Dot-call style (same result, closer to Rust's method chaining)
let b = [1, 2, 3, 4, 5]
.filter((x) => x % 2 == 0)
.map((x) => x * 10)
println(a == b)
}
Unlike Rust, hica’s dot-call isn’t limited to methods defined in impl blocks — any function can be called with dot syntax. a.f(b) desugars to f(a, b).
Loops
Rust has loop, while, and for with break/continue:
for x in &[1, -2, 3] {
if *x < 0 { continue; }
println!("{}", x);
}
let result = loop {
break 42;
};
Rust’s loop can return a value via break expr. Labeled breaks ('outer: loop { break 'outer; }) allow breaking from nested loops.
hica has the same set: while, for, repeat, loop, break, and continue:
for x in [1, -2, 3] {
if x < 0 { continue }
println(x)
}
loop {
break
}
hica’s break cannot return a value and there are no labeled breaks. break/continue always apply to the innermost loop.
Bitwise Operations
Rust uses infix operators and works across multiple integer types:
let flags: u8 = 0b1010_1100;
let masked = flags & 0x0F; // AND
let shifted = flags >> 4; // shift right
let flipped = flags ^ 0xFF; // XOR
let complement = !flags; // NOT
hica uses named functions. All operations work on 32-bit integers internally:
fun main() {
let flags = 0b1010_1100
let masked = bit_and(flags, 0x0F)
let shifted = bit_shr(flags, 4)
let flipped = bit_xor(flags, 0xFF)
let complement = bit_not(flags)
}
Rust’s operators are familiar to C programmers and work on every integer type (u8, i32, u64, etc.). hica has a single int type with named functions — less flexible, but no concerns about integer width mismatches.
hica adds bit-level pattern matching with ? wildcards, inspired by hardware description languages:
match opcode {
0b11??_???? => "category 3",
0b10??_???? => "category 2",
_ => "other"
}
Rust has no direct equivalent — you’d write explicit mask-and-compare guards.
Compilation and Performance
Rust compiles through LLVM to native code with fine-grained control over performance, allocation, and inlining.
hica compiles through Koka to C. The resulting binaries are fast in practice, but without fine-grained control over allocation patterns, inlining, or memory layout.
Ecosystem
Rust has crates.io with over 150,000 packages, extensive documentation, an active community, and production use at major companies. hica is a new language with a small but growing set of examples, backed by the Koka standard library. Rust wins here by orders of magnitude.
Conclusion
Rust is the right choice when you need zero-cost abstractions, fine-grained memory control, and a battle-tested ecosystem for production systems.
hica is the right choice when you want the same values (immutability, type safety, expression-oriented design) without the learning curve of ownership and lifetimes. It’s a good stepping stone toward Rust, or a simpler alternative when automatic reference counting is sufficient and you don’t need fine-grained control over memory or performance.
If you already know Rust, you’ll read hica code fluently. If you’re learning hica first, you’ll find that many of its patterns (Result types, match expressions, immutability) transfer directly when you’re ready for Rust.