Functional Programming in hica
Functional programming (FP) is a style where you build programs by composing functions rather than by writing sequences of instructions that change state. hica is designed around this style: immutable data, expressions everywhere, and functions as first-class values.
All examples are runnable. You don’t need a background in FP; if you can write a function and read a match expression, you have enough.
Expressions, not statements
In most languages, statements do things and expressions produce values. In hica, almost everything is an expression, including if, match, and blocks. That means you can use them anywhere a value is expected:
fun sign(x) => if x < 0 { "negative" } else { "non-negative" }
The body of a {} block is the value of its last expression. No return keyword:
fun clamp(x, lo, hi) {
if x < lo { lo }
else if x > hi { hi }
else { x }
}
This single rule carries you through most of FP: when everything has a value, everything can be composed.
Immutability by default
FP avoids shared mutable state. When you can’t change a value after creating it, your functions are easier to reason about and test.
hica uses let for immutable bindings:
let name = "Alicia"
let scores = [85, 92, 78]
You never mutate scores in place. Instead, you create new lists:
let updated = scores + [95] // new list: [85, 92, 78, 95]
let doubled = map(scores, (x) => x * 2)
When you need mutation (a counter, a loop variable), use var. It’s locally scoped and can’t leak out of the function:
var total = 0
for x in scores {
total = total + x
}
var is opt-in. Everything else stays immutable.
Pure functions
A pure function always returns the same output for the same input and has no side effects (no printing, no file I/O, no mutable state). Pure functions are easy to test and compose.
fun add(a, b) => a + b
fun square(x) => x * x
fun to_celsius(f: float) => (f - 32.0) * 5.0 / 9.0
In hica, functions are pure by default. The type system (inherited from Koka) tracks effects like I/O, so when a function does have a side effect, that’s visible in its type.
Pure functions are what you build with. Everything else is composition.
Functions as first-class values
In FP, functions are values like integers or strings. You can store them in variables, pass them to other functions, and return them from functions.
fun apply(f, x: int) => f(x)
fun main() {
let double = (x) => x * 2
let greet = (name) => "Hello, " + name
println(apply(double, 5)) // 10
println(greet("Olle")) // Hello, Olle
}
A function that takes or returns another function is called a higher-order function. apply above is one.
Closures
A closure is a function that captures variables from its surrounding scope:
fun make_adder(n) => (x) => x + n
fun main() {
let add5 = make_adder(5)
let add10 = make_adder(10)
println(add5(3)) // 8
println(add10(3)) // 13
}
make_adder returns a new function each time. That function closes over n, meaning it remembers the value of n from when it was created, even after make_adder has returned.
This pattern lets you stamp out specialised functions on demand:
fun make_multiplier(factor) => (x) => x * factor
fun main() {
let triple = make_multiplier(3)
let nums = [1..5]
println(map(nums, triple)) // [3, 6, 9, 12, 15]
}
map, filter, fold
These three functions cover most of what you need for working with lists.
map: transform every element
fun main() {
let nums = [1..5]
println(map(nums, (x) => x * x)) // [1, 4, 9, 16, 25]
println(map(nums, show)) // ["1", "2", "3", "4", "5"]
}
map takes a list and a function. It applies the function to each element and returns a new list of the same length.
filter: keep matching elements
fun main() {
let nums = [1..8]
let evens = filter(nums, (x) => x % 2 == 0)
println(evens) // [2, 4, 6, 8]
}
filter keeps only the elements for which the predicate returns true.
fold: reduce to a single value
fun main() {
let nums = [1..5]
let total = fold(nums, 0, (acc, x) => acc + x)
println(total) // 15
let product = fold(nums, 1, (acc, x) => acc * x)
println(product) // 120
}
fold accumulates a result. It starts with an initial value and applies the function to each element in turn: acc holds the running result, x is the current element.
You can implement many list operations with fold:
fun my_length(xs) => fold(xs, 0, (acc, _) => acc + 1)
fun my_max(xs) => fold(xs, 0, (acc, x) => if x > acc { x } else { acc })
fun my_reverse(xs) => fold(xs, [], (acc, x) => [x] + acc)
flatten and flat_map
map transforms every element. But sometimes the function you pass to map itself returns a list. The result is a list of lists, which is usually not what you want:
fun main() {
let sentences = ["hello world", "foo bar", "one two three"]
let split_words = map(sentences, (s) => split(s, " "))
println(split_words)
// [["hello", "world"], ["foo", "bar"], ["one", "two", "three"]]
}
You wanted a flat list of all words. Two functions solve this.
concat: collapse one level of nesting
fun main() {
let nested = [[1, 2], [3, 4], [5, 6]]
println(concat(nested)) // [1, 2, 3, 4, 5, 6]
}
concat takes a list of lists and collapses one level. (Other languages call this flatten.)
flat_map: map and flatten in one step
flat_map applies a function to each element and joins all the resulting lists together. It is map followed by concat, but written as one step:
fun main() {
let sentences = ["hello world", "foo bar", "one two three"]
let all_words = flat_map(sentences, (s) => split(s, " "))
println(all_words)
// ["hello", "world", "foo", "bar", "one", "two", "three"]
}
You reach for flat_map whenever the function you are mapping returns a list.
Expanding elements in a pipe
flat_map fits naturally in a pipe. Use it at the step where a single element becomes multiple elements:
fun expand(n) => [n, n * 10] // each element fans out to two
fun main() {
let result = [1, 2, 3]
|> flat_map(expand) // [1, 10, 2, 20, 3, 30]
|> filter((x) => x > 5) // [10, 20, 30]
println(result)
}
Without flat_map you would get [[1, 10], [2, 20], [3, 30]] and filter would be operating on lists, not numbers.
The same idea applies to Maybe
The pattern — apply a function that produces a wrapped value, then flatten the wrapping — appears with Maybe too. map_maybe transforms the value inside a Maybe. But if the function itself returns Maybe, you’d end up with Maybe<Maybe<x>>. and_then prevents that by flattening the extra layer automatically:
fun parse_pos(s: string) : maybe<int> {
let n = parse_int(s)?
if n > 0 { Some(n) } else { None }
}
fun main() {
// and_then chains steps that each return Maybe — no nesting, no nested match
let result = Some("42")
|> and_then((s) => parse_int(s)) // parse string → maybe<int>
|> and_then((n) => parse_pos(show(n)))
|> map_maybe((n) => n * 2)
println(result) // Just(84)
let bad = Some("-5")
|> and_then((s) => parse_int(s))
|> and_then((n) => parse_pos(show(n)))
|> map_maybe((n) => n * 2)
println(bad) // Nothing
}
flat_map for lists and and_then for Maybe are the same idea with different names. Functional programmers call this operation bind. Knowing the pattern — “map over a wrapped value with a function that itself returns a wrapped value, and don’t double-wrap” — is the thing to take away, whatever the type.
Composition with |>
The pipe operator |> feeds the result of one expression into the next function. It reads left to right, matching the order of operations:
fun main() {
let result = [1..10]
|> filter((x) => x % 2 == 0) // [2, 4, 6, 8, 10]
|> map((x) => x * x) // [4, 16, 36, 64, 100]
|> fold(0, (acc, x) => acc + x) // 220
println(result)
}
Without |> you’d write:
let result = fold(map(filter([1..10], (x) => x % 2 == 0), (x) => x * x), 0, (acc, x) => acc + x)
With |>, each step is on its own line and you read the transformation in the order it happens. That’s the point: each step is one function, and they chain.
Point-free style
When a lambda just passes its argument directly to a function, you can drop the lambda entirely:
fun is_even(x) => x % 2 == 0
fun square(x) => x * x
fun main() {
let result = [1..5]
|> filter(is_even) // same as filter(nums, (x) => is_even(x))
|> map(square)
println(result) // [4, 16]
}
This style (naming the function rather than wrapping it in a lambda) is called point-free. Use it when the name says more than the lambda would.
Recursion
FP uses recursion where imperative code uses loops. A recursive function calls itself with a smaller input until it hits a base case:
fun sum(xs) => match xs {
[] => 0,
[x, ..rest] => x + sum(rest)
}
fun main() {
println(sum([1..5])) // 15
}
The [x, ..rest] pattern splits a list into its first element and the rest. This pairs naturally with recursion:
fun contains(xs, target) => match xs {
[] => false,
[x, ..rest] => x == target || contains(rest, target)
}
fun map_r(xs, f) => match xs {
[] => [],
[x, ..rest] => [f(x)] + map_r(rest, f)
}
For mutual recursion (two functions that call each other), no forward declarations are needed:
fun is_even(n) => if n == 0 { true } else { is_odd(n - 1) }
fun is_odd(n) => if n == 0 { false } else { is_even(n - 1) }
In practice you’ll use map/filter/fold far more than explicit recursion, but recursion is the right tool for tree-shaped data, and knowing it helps you read other people’s code.
Algebraic data types
Functional programming uses algebraic data types (ADTs) to model data that comes in different shapes. hica has two kinds.
Structs: product types
A struct bundles fields together:
struct Point { x: int, y: int }
struct Person { name: string, age: int }
fun greet(p: Person) => "Hi, {p.name}!"
fun distance(a: Point, b: Point) : int {
let dx = a.x - b.x
let dy = a.y - b.y
dx * dx + dy * dy
}
Structs are immutable. To “update” a field, create a new struct:
struct Player { name: string, score: int }
fun add_score(p: Player, points: int) : Player =>
Player { name: p.name, score: p.score + points }
Enums: sum types
An enum represents a choice between distinct variants, each of which can carry its own data:
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
}
The compiler checks exhaustiveness: if you forget a variant, you get a warning. This makes adding new variants safe; the compiler tells you every place that needs updating.
Enums can be recursive, which is how you model tree-shaped data:
type Tree {
Leaf,
Node(value: int, left: Tree, right: Tree)
}
fun tree_sum(t: Tree) : int => match t {
Leaf => 0,
Node(v, l, r) => v + tree_sum(l) + tree_sum(r)
}
Maybe and Result
Two built-in types handle failure without exceptions.
Maybe: a value that might not exist
fun find_first(xs, pred) => match xs {
[] => None,
[x, ..rest] => if pred(x) { Some(x) } else { find_first(rest, pred) }
}
fun main() {
let nums = [1, 3, 5, 4, 7]
match find_first(nums, (x) => x % 2 == 0) {
Some(n) => println("First even: {n}"),
None => println("No evens found")
}
}
Some(x) wraps a value; None signals absence. The compiler forces you to handle both cases.
Chaining with combinators
Nested match for every step gets unwieldy fast. Combinators keep the chain flat. There is also a subtlety: when the function you want to apply itself returns Maybe, using map_maybe would give you Maybe<Maybe<x>>. and_then prevents the double-wrapping by flattening one level — the same job flat_map does for lists:
fun main() {
// map_maybe transforms the value inside, leaves None alone
let x = Some(21) |> map_maybe((n) => n * 2)
println(x) // Just(42)
// and_then chains a function that itself returns Maybe
let y = Some("42")
|> and_then((s) => parse_int(s))
|> map_maybe((n) => n + 1)
println(y) // Just(43)
// Short-circuits at the first None
let z = Some("nope")
|> and_then((s) => parse_int(s))
|> map_maybe((n) => n + 1)
println(z) // Nothing
}
Result: success or a specific error
Result carries an error message on failure:
fun safe_divide(a, b) =>
if b == 0 { Err("division by zero") }
else { Ok(a / b) }
fun validate_positive(n) =>
if n > 0 { Ok(n) }
else { Err("must be positive") }
fun main() {
let result = safe_divide(100, 4)
|> and_then_result((n) => validate_positive(n))
|> map_result((n) => n * 2)
println(result) // Right(50)
let bad = safe_divide(100, 0)
|> and_then_result((n) => validate_positive(n))
println(bad) // Left("division by zero")
}
The ? operator
For functions that chain many fallible steps, ? keeps the code flat. It returns early with None or Err if a step fails:
fun add_strings(a: string, b: string) : maybe<int> {
let x = parse_int(a)?
let y = parse_int(b)?
Some(x + y)
}
fun main() {
println(add_strings("10", "32")) // Just(42)
println(add_strings("10", "oops")) // Nothing
}
Three constraints to know:
- The return type annotation is required on the enclosing function: the compiler needs it to emit the early return correctly.
- The wrapper type must match:
?onmaybeonly works inside amaybe-returning function, and?onresultonly inside aresult-returning function. ?cannot be used inmain():main()returns(). Move fallible logic into a helper and call it frommain()withmatch.
Putting it together
A program that pulls it all together: structs, pure functions, closures, pipe, pattern matching, and maybe.
struct Student { name: string, grade: int }
fun letter_grade(g: int) : string => match g {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F"
}
fun passing(s: Student) : bool => s.grade >= 60
fun summarise(students: list<Student>) {
let passing_students = filter(students, passing)
let names = map(passing_students, (s) => s.name)
let avg = fold(passing_students, 0, (acc, s) => acc + s.grade)
/ length(passing_students)
println("Passing: {join(names, ", ")}")
println("Average grade (passing): {avg}")
println("Letter grade: {letter_grade(avg)}")
}
fun main() {
let students = [
Student { name: "Alicia", grade: 92 },
Student { name: "Björn", grade: 55 },
Student { name: "Cecilia", grade: 78 },
Student { name: "David", grade: 61 }
]
summarise(students)
}
Output:
Passing: Alicia, Cecilia, David
Average grade (passing): 77
Letter grade: C
Key ideas to take with you
| Concept | hica |
|---|---|
| Immutable data | let by default, var when you need it |
| First-class functions | (x) => x * 2, fun add(a, b) => a + b |
| Closures | fun make_adder(n) => (x) => x + n |
| Composition | \|> pipe operator |
| Transforming lists | map, filter, fold |
| Flattening / expanding | concat (flatten), flat_map (map + flatten) |
| Chaining wrapped values | and_then (Maybe), and_then_result (Result) — same idea as flat_map |
| Recursive data | type Tree { Leaf, Node(...) } |
| Safe failures | Maybe (Some/None) and Result (Ok/Err) |
| Exhaustive matching | match with compiler-checked variants |
The point isn’t to avoid loops. It’s that small functions composed together tend to be easier to test, name, and reuse. hica’s design makes that the natural default.