Functional Programming in hica

Hacker News Top Tools

Summary

An introduction to functional programming in the hica language, covering expressions, immutability, pure functions, closures, and higher-order functions like map, filter, and fold.

No content available
Original Article
View Cached Full Text

Cached at: 07/05/26, 01:31 PM

# Functional Programming in hica - hica Source: [https://www.hica.dev/docs/functional-programming/](https://www.hica.dev/docs/functional-programming/) 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) // Some(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) // None } ``` `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) // Some(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) // Some(43) // Short-circuits at the first None let z = Some("nope") |> and_then((s) => parse_int(s)) |> map_maybe((n) => n + 1) println(z) // None } ``` ### 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) // Ok(50) let bad = safe_divide(100, 0) |> and_then_result((n) => validate_positive(n)) println(bad) // Err("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")) // Some(42) println(add_strings("10", "oops")) // None } ``` 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:`?`on`maybe`only works inside a`maybe`\-returning function, and`?`on`result`only inside a`result`\-returning function\. - `?`**cannot be used in`main\(\)`**:`main\(\)`returns`\(\)`\. Move fallible logic into a helper and call it from`main\(\)`with`match`\. ## 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 ConcepthicaImmutable data`let`by default,`var`when you need itFirst\-class functions`\(x\) =\> x \* 2`,`fun add\(a, b\) =\> a \+ b`Closures`fun make\_adder\(n\) =\> \(x\) =\> x \+ n`Composition`\\\|\>`pipe operatorTransforming 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 variantsThe 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\.

Similar Articles

Prism: An Impure Functional Language With Typed Effects

Lobsters Hottest

Prism is a new functional language that integrates algebraic effects with a type system, allowing mutable state and other effects to be used without monads while preserving purity externally. It aims to make effects a first-class part of the type system, enabling optimization and safe usage.

Show HN: Hsrs – Type-Safe Haskell Bindings Generator for Rust

Hacker News Top

Hsrs is a type-safe FFI bindings generator that allows Rust code to be called from Haskell with automatic memory management, type conversions, and Borsh serialization. It provides annotations in Rust and generates idiomatic Haskell wrappers.