First-Class Functions: closures, currying, and higher-order style
How eight languages treat functions as values - capturing closures, currying arguments, and threading behaviour through higher-order pipelines.
A function is first-class when you can store it in a variable, pass it to another function, and return it from one. That single property reshapes how a language reads. Once functions are values, three idioms follow almost automatically: closures (functions that capture their surrounding bindings), currying (turning an n-argument function into a chain of one-argument functions), and higher-order style (functions parameterised by other functions). The eight languages here all support the trio, but they disagree about what is free, what costs syntax, and what the type system demands in return.
Currying: built in, bolted on, or faked
Haskell makes currying the default and the only model. Every function of "two arguments" is really a function returning a function, and the type Int -> Int -> Int is read right-associatively as Int -> (Int -> Int). Partial application is therefore free - just supply fewer arguments:
add :: Int -> Int -> Int
add x y = x + y
add5 :: Int -> Int -- partial application, no special syntax
add5 = add 5
OCaml shares this calculus exactly: let add x y = x + y has type int -> int -> int, and add 5 is a valid value. The currying is so ingrained that the multi-argument call add 5 10 is two applications under the hood.
The other languages curry by hand or by helper. Python reaches for functools.partial, and the snippet below runs as written:
from functools import partial
def add(x, y): return x + y
add5 = partial(add, 5)
print(add5(10)) # 15
Perl builds the chain explicitly with nested subs, and the arrow operator applies each stage:
sub add { my $x = shift; return sub { my $y = shift; $x + $y } }
print add(3)->(4), "\n"; # 7
Raku offers the most ergonomic non-default currying. The .assuming method fixes leading arguments, and the &-sigil names a routine as a value: my &add5 = &infix:<+>.assuming(5). Its WhateverCode lets * + 5 stand in for a one-argument lambda, so partial behaviour often needs no helper at all.
Guji, an in-house compiled, statically typed, functional-first language (v0.1-alpha), curries through explicit nested closures rather than auto-currying. A function that returns a sub is the currying primitive, and the result is applied stage by stage. The following compiles and runs identically under both the reference interpreter and the native AOT compiler:
sub make_adder($n: Int) {
sub($y: Int): Int { $n + $y } # captures $n
}
sub main(): Unit {
$inc = make_adder(1)
print($inc(10)) # 11
}
Notice the outer sub needs no return-type annotation - guji infers that make_adder yields an Int -> Int. That inference is what keeps a statically typed language from feeling ceremonious when functions return functions.
Go is the outlier on currying: it has no partial-application sugar and, before generics, could not even write a reusable compose without interface{}. You curry by returning closures literally, much like Perl, and you accept the verbosity.
Closures: what gets captured, and how
Capture is where the languages diverge most sharply, because capture forces a decision about mutability and lifetime.
Python, Perl, Raku, OCaml, and Haskell all capture by reference to the enclosing binding, and the runtime keeps that binding alive as long as the closure exists. Haskell's purity makes this trivially safe: captured values are immutable, so there is no question of a closure observing a later mutation. The verified Perl closure below keeps $n alive past the call that created it:
sub make_adder { my ($n) = @_; return sub { $n + $_[0] } }
my $inc = make_adder(1);
print $inc->(10), "\n"; # 11
Guji captures immutable bindings from the enclosing scope, and its semantics are precise about freshness: each evaluation of a lambda expression allocates a new environment, so the same lambda AST run twice in a loop yields two independent closures. That detail matters for a compiled language with reference-counted values - the native backend must allocate a fresh capture env rather than collapsing to a shared static slot. Guji enforces this so closures escaping a loop body each see their own captured value.
Rust is the language that makes capture a first-class type decision. A closure implements one of three traits depending on how it uses its captures: Fn (borrows immutably and can be called repeatedly), FnMut (borrows mutably), or FnOnce (consumes captures, callable once). The move keyword forces capture by value, which is how a closure outlives the stack frame that built it:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |y| n + y // move: n is captured by value
}
Returning impl Fn(...) is Rust's way of saying "some concrete closure type satisfying Fn". A boxed Box<dyn Fn(i32) -> i32> gives heap-allocated dynamic dispatch when you need to store heterogeneous closures. No other language here asks you to name the capture discipline, and none gives you the same compile-time guarantee that a borrowing closure cannot dangle.
Go closures capture variables by reference, which historically bit programmers in for loops where every closure shared the one loop variable - a hazard Go fixed by giving each iteration its own variable in Go 1.22. The lesson rhymes with guji's per-evaluation environment: shared capture slots are a footgun, and a careful language allocates fresh ones.
Higher-order style: pipelines and the shape of data flow
Higher-order functions are where first-class functions pay off daily. The canonical trio is map, filter, and reduce (fold). Haskell writes these as curried library functions designed for composition, and laziness lets map f . filter p fuse over infinite lists without building intermediates. OCaml's List.map, List.filter, and List.fold_left are eager equivalents, often pipelined with the |> operator that Raku, Elm, and F# also adopted.
Guji puts these methods on lists directly, and the brace lambda { $_ ... } supplies an implicit topic variable. The following chain compiles natively and prints 35:
@nums = [1, 2, 3, 4, 5]
$r = @nums.map({ $_ * $_ }).filter({ $_ % 2 == 1 }).sum()
print("odd squares sum: { $r }") # 1 + 9 + 25 = 35
It also takes a named sub as a higher-order argument - apply_twice($double, 5) returns 20 - and function-valued parameters are simply written $f with their type inferred at the call site. List length is .count(), and the same idioms back guji's reduce, sort_by, and find. Worth noting at v0.1-alpha: the interpreter is the broader of the two backends. A returned closure that calls a captured function parameter (a hand-rolled compose) runs under the interpreter today but is not yet supported by native codegen, which the two-target parity discipline tracks as a known gap rather than a divergence.
Python expresses the same pipeline with comprehensions, which most Pythonistas prefer to nested map/filter; both forms below are verified:
nums = [1, 2, 3, 4, 5]
print(sum(x * x for x in nums if x % 2 == 1)) # 35
Perl leans on map and grep as list operators with a block argument, reading right-to-left, while Raku elevates the same idea: @nums.map(* ** 2).grep(* %% 2) uses WhateverCode for the lambdas and a chained method syntax. Raku's design philosophy - "there's more than one way to do it" - shows plainly here, since the very same transform can be a method chain, a feed operator ==>, or a hyperoperator.
Rust's iterator adapters (.map(), .filter(), .fold()) are lazy and zero-cost: they compile down to the equivalent hand-written loop, so higher-order style carries no runtime penalty - the one place where functional abstraction and bare-metal performance fully reconcile. Go, lacking iterator adapters in its standard library until very recently, tended to write explicit loops, and even with generics its higher-order code stays comparatively verbose.
The trade
The spectrum runs from "currying is the universe" (Haskell, OCaml) to "functions are values but you wire the plumbing yourself" (Go, Perl). In between, Rust trades syntax for guarantees, Python and Raku trade guarantees for ergonomics, and guji aims at a compiled middle - static types and AOT native code, yet closures, inference, and method-chained higher-order pipelines that read like a scripting language. Whichever end you stand at, the same small idea is doing the work: a function you can hold in your hand is a function you can build with.