← Code Compare

Closures

A closure is a function that captures variables from the scope where it was defined and keeps them alive after that scope returns. The same task in every language: a factory make_between(lo, hi) that returns a one-argument predicate closing over both bounds, then uses it to filter a list down to [3, 4, 5, 6, 7]. Watch how each language spells the captured function (a named lambda keyword? an arrow? move? a bare sub?) and whether the capture is by value or by reference - the detail that decides what a closure sees when its surrounding variables change.

Show: GujiGoOCamlHaskellPerlRakuRustPython
Guji
# make_between returns an anonymous sub that captures BOTH $lo and $hi
sub make_between($lo: Int, $hi: Int) {
    sub($x: Int) { $x >= $lo && $x <= $hi }
}

sub main() {
    $in_range = make_between(3, 7)
    @nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    # the closure is a first-class predicate, passed to filter via a topic block
    @kept = @nums.filter({ $in_range($_) })
    print("kept: { @kept }")
    print("in_range(5) = { $in_range(5) }")
    print("in_range(8) = { $in_range(8) }")
}

Because functions are first-class values, make_between simply returns an anonymous sub that closes over the two parameters $lo and $hi. Guji captures immutable bindings - which is most of them, since bindings are immutable by default - so a closure always sees a stable snapshot and can never race on shared state. Capturing a mut binding is in fact a compile-time error (cannot capture mut binding ... in a closure), so Guji has no equivalent of a mutating counter closure; stateful work is threaded as values instead. The closure here is used data-first as a predicate inside filter's topic block.

Go
package main

import "fmt"

// makeBetween returns a closure capturing both lo and hi.
func makeBetween(lo, hi int) func(int) bool {
	return func(x int) bool { return x >= lo && x <= hi }
}

func main() {
	inRange := makeBetween(3, 7)
	nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	var kept []int
	for _, x := range nums {
		if inRange(x) {
			kept = append(kept, x)
		}
	}
	fmt.Println("kept:", kept)
	fmt.Printf("in_range(5) = %v\n", inRange(5))
	fmt.Printf("in_range(8) = %v\n", inRange(8))
}

Go has true closures: the function literal returned by makeBetween captures lo and hi by reference, so a for-loop variable reused across captures is a classic Go footgun (fixed by the per-iteration loop scoping in Go 1.22). The closure type is written out explicitly as func(int) bool. With no filter in the standard library, the predicate is applied in an explicit for ... range loop that appends matches - the conventional Go spelling.

OCaml
(* make_between returns a closure capturing both lo and hi. *)
let make_between lo hi = fun x -> x >= lo && x <= hi

let () =
  let in_range = make_between 3 7 in
  let nums = [1; 2; 3; 4; 5; 6; 7; 8; 9] in
  let kept = List.filter in_range nums in
  Printf.printf "kept: [%s]\n"
    (String.concat "; " (List.map string_of_int kept));
  Printf.printf "in_range(5) = %b\n" (in_range 5);
  Printf.printf "in_range(8) = %b\n" (in_range 8)

Every OCaml function is curried, so make_between lo hi returning fun x -> ... is the natural closure - it could even be written let make_between lo hi x = ... and partially applied. Captured bindings are immutable values, much like Guji, so the closure sees an unchanging lo/hi. Types are fully inferred (in_range : int -> bool), and List.filter takes the predicate first and the list second - the reverse of Guji's data-first order.

Haskell
-- makeBetween returns a closure capturing both lo and hi.
makeBetween :: Int -> Int -> (Int -> Bool)
makeBetween lo hi = \x -> x >= lo && x <= hi

main :: IO ()
main = do
  let inRange = makeBetween 3 7
      nums    = [1 .. 9]
      kept    = filter inRange nums
  putStrLn ("kept: " ++ show kept)
  putStrLn ("in_range(5) = " ++ show (inRange 5))
  putStrLn ("in_range(8) = " ++ show (inRange 8))

The returned lambda \x -> ... closes over lo and hi, and since every Haskell binding is immutable, the capture is effectively by value - the closure always sees a stable snapshot, like Guji and OCaml. Currying makes this even more natural: makeBetween lo hi could be written makeBetween lo hi x = ... and partially applied. filter inRange nums takes the predicate first, the standard Prelude order.

Perl
use strict;
use warnings;

# make_between returns a sub that closes over the lexical $lo and $hi.
sub make_between {
    my ($lo, $hi) = @_;
    return sub { my ($x) = @_; $lo <= $x && $x <= $hi };
}

my $in_range = make_between(3, 7);
my @nums = (1, 2, 3, 4, 5, 6, 7, 8, 9);
my @kept = grep { $in_range->($_) } @nums;
print "kept: [", join(", ", @kept), "]\n";
print "in_range(5) = ", ($in_range->(5) ? "true" : "false"), "\n";
print "in_range(8) = ", ($in_range->(8) ? "true" : "false"), "\n";

An anonymous sub { ... } closes over the lexical my $lo and my $hi, which is exactly how Perl closures work - each call to make_between makes a fresh pair of captured lexicals. The code reference is invoked with the ->() arrow. grep { ... } @list runs its block with each element bound to the topic $_, the same topic idea Guji spells $_ inside { ... }. Larry Wall designed these lexical closures into Perl 5 precisely because they keep the captured scope alive.

Raku
# make-between returns a pointy block closing over both $lo and $hi.
sub make-between(Int $lo, Int $hi) {
    -> Int $x { $lo <= $x <= $hi }
}

my &in-range = make-between(3, 7);
my @nums = 1, 2, 3, 4, 5, 6, 7, 8, 9;
my @kept = @nums.grep(&in-range);
say "kept: @kept[]";
say "in_range(5) = { in-range(5) }";
say "in_range(8) = { in-range(8) }";

Raku makes the closure a pointy block -> Int $x { ... }, stored in a &-sigilled variable so it calls like a sub and can be passed straight to .grep(&in-range). The chained comparison $lo <= $x <= $hi reads like the maths. Sigils and the optional gradual types (Int) make Raku the most visible ancestor of Guji's $/@/% and its closure-as-value style.

Rust
// make_between returns a closure; `move` captures lo and hi by value.
fn make_between(lo: i32, hi: i32) -> impl Fn(i32) -> bool {
    move |x| x >= lo && x <= hi
}

fn main() {
    let in_range = make_between(3, 7);
    let nums = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    let kept: Vec<i32> = nums.iter().copied().filter(|&x| in_range(x)).collect();
    println!("kept: {:?}", kept);
    println!("in_range(5) = {}", in_range(5));
    println!("in_range(8) = {}", in_range(8));
}

Rust closures are |x| ...; returning one needs impl Fn(i32) -> bool plus move so the closure owns the captured lo and hi rather than borrowing values that would not outlive the function. Filtering is lazy and iterator-based: iter().filter(...).collect() only runs when collected into a Vec. The explicit Fn trait bound and move reflect Rust's no-GC ownership model - the capture mode that Guji and OCaml leave implicit.

Python
def make_between(lo, hi):
    return lambda x: lo <= x <= hi  # closure capturing both lo and hi

in_range = make_between(3, 7)
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]
kept = [x for x in nums if in_range(x)]
print(f"kept: {kept}")
print(f"in_range(5) = {in_range(5)}")
print(f"in_range(8) = {in_range(8)}")

Python closures are everyday: make_between returns a lambda that captures the enclosing lo and hi. Capture is by reference to the enclosing binding, looked up when the closure runs - the source of the well-known late-binding surprise when closures are built in a loop. lambda is limited to a single expression, so anything larger becomes a nested def. Rather than filter, the idiomatic spelling is a comprehension, [x for x in nums if in_range(x)].