← Code Compare

Sorting: by natural order and by a custom key

The same task in all eight languages: sort a list of integers into ascending order, then sort a list of people by a custom key - their age, descending. Watch how each language expresses the key: a function that extracts the field to compare (Guji, Python, Rust, Raku), a two-argument comparator that returns the ordering (Go, OCaml, Perl), or both. Note too whether sorting returns a new list or rearranges the original in place.

Show: GujiGoOCamlHaskellPerlRakuRustPython
Guji
class Person {
    has $.name: Str
    has $.age: Int
}

sub main() {
    @nums = [3, 1, 4, 1, 5, 9, 2, 6]
    @asc = @nums.sort()
    print("ascending: @asc")

    @people = [
        Person(name: "Ada", age: 36),
        Person(name: "Bo",  age: 19),
        Person(name: "Cy",  age: 28),
    ]
    @oldest = @people.sort_by({ $_.age }).reverse()
    for $p in @oldest {
        print("{ $p.name } is { $p.age }")
    }
}

sort() returns a new ordered copy in natural order, and sort_by takes a key function - a topic block { $_.age } whose implicit parameter is $_ (§7.4) - that extracts the value to compare. There is no comparator that returns an ordering; sorting is always by an ascending key, so descending is expressed by sorting then .reverse(), all chained data-first with . (§7.2). Bindings are immutable (§4), so every step yields a fresh list rather than rearranging @people.

Go
package main

import (
	"fmt"
	"slices"
	"sort"
)

type Person struct {
	Name string
	Age  int
}

func main() {
	nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
	slices.Sort(nums)
	fmt.Println("ascending:", nums)

	people := []Person{{"Ada", 36}, {"Bo", 19}, {"Cy", 28}}
	sort.Slice(people, func(i, j int) bool {
		return people[i].Age > people[j].Age
	})
	for _, p := range people {
		fmt.Printf("%s is %d\n", p.Name, p.Age)
	}
}

slices.Sort (Go 1.21+) sorts an ordered slice in place. For the custom order, sort.Slice takes a less(i, j) predicate that returns whether element i should sort before j; returning people[i].Age > people[j].Age gives a descending sort directly, with no separate reverse step. Both mutate the slice rather than returning a copy; slices.SortFunc is the newer comparator-returning variant.

OCaml
type person = { name : string; age : int }

let () =
  let nums = [3; 1; 4; 1; 5; 9; 2; 6] in
  let asc = List.sort compare nums in
  Printf.printf "ascending: %s\n"
    (String.concat " " (List.map string_of_int asc));

  let people =
    [ { name = "Ada"; age = 36 };
      { name = "Bo"; age = 19 };
      { name = "Cy"; age = 28 } ]
  in
  let oldest = List.sort (fun a b -> compare b.age a.age) people in
  List.iter (fun p -> Printf.printf "%s is %d\n" p.name p.age) oldest

List.sort takes a comparator returning a negative, zero, or positive int, and produces a new sorted list (the input is unchanged). The polymorphic compare gives natural order for the integers; for the people, swapping the arguments - compare b.age a.age - inverts the result and so sorts by age descending. There is no built-in key-function form, so the field extraction lives inside the comparator.

Haskell
import Data.List (sortBy, sortOn)
import Data.Ord (Down (Down))
import Text.Printf (printf)

data Person = Person { name :: String, age :: Int }

main :: IO ()
main = do
  let nums = [3, 1, 4, 1, 5, 9, 2, 6] :: [Int]
  putStrLn ("ascending: " ++ show (sortOn id nums))

  let people = [ Person "Ada" 36, Person "Bo" 19, Person "Cy" 28 ]
      oldest = sortOn (Down . age) people
  mapM_ (\p -> printf "%s is %d\n" (name p) (age p)) oldest

Sorting returns a brand-new list - everything is immutable, so there is no in-place variant. sortOn takes a key function (Schwartzian-transform style, computing each key once) and Data.Ord.Down wraps the key to reverse just that field, giving an age-descending sort with no separate reverse pass. The natural order comes from the element's Ord instance (sort/sortOn id); sortBy is the comparator-returning form when a key function is not enough.

Perl
use strict;
use warnings;

my @nums = (3, 1, 4, 1, 5, 9, 2, 6);
my @asc = sort { $a <=> $b } @nums;
print "ascending: @asc\n";

my @people = (
    { name => "Ada", age => 36 },
    { name => "Bo",  age => 19 },
    { name => "Cy",  age => 28 },
);
my @oldest = sort { $b->{age} <=> $a->{age} } @people;
print "$_->{name} is $_->{age}\n" for @oldest;

Perl's sort takes a comparator block that compares the package globals $a and $b; <=> is the numeric three-way operator (use cmp for strings). Default sort is string order, so the explicit $a <=> $b is needed even for the integers. Reversing the operands to $b <=> $a flips it to descending, and dereferencing $b->{age} pulls the key out of each hashref inline. sort returns a new list and leaves the input alone.

Raku
class Person { has $.name; has $.age; }

my @nums = 3, 1, 4, 1, 5, 9, 2, 6;
say "ascending: ", @nums.sort.list;

my @people =
    Person.new(name => "Ada", age => 36),
    Person.new(name => "Bo",  age => 19),
    Person.new(name => "Cy",  age => 28);

my @oldest = @people.sort(*.age).reverse;
say "{.name} is {.age}" for @oldest;

Raku's sort accepts either a one-argument key function or a two-argument comparator; here *.age is a Whatever-star closure that extracts the key, and bare .sort uses each element's own value. sort returns a new sorted sequence and never mutates the original, so descending is just .reverse chained on, mirroring Guji. Inside the string, {.age} interpolates a method call on the loop topic $_.

Rust
use std::cmp::Reverse;

struct Person {
    name: &'static str,
    age: u32,
}

fn main() {
    let mut nums = [3, 1, 4, 1, 5, 9, 2, 6];
    nums.sort();
    println!("ascending: {nums:?}");

    let mut people = vec![
        Person { name: "Ada", age: 36 },
        Person { name: "Bo", age: 19 },
        Person { name: "Cy", age: 28 },
    ];
    people.sort_by_key(|p| Reverse(p.age));
    for p in &people {
        println!("{} is {}", p.name, p.age);
    }
}

Rust sorts slices in place and requires a mut binding to do so. sort() uses the type's natural Ord; sort_by_key takes a key-extracting closure, and wrapping the key in std::cmp::Reverse flips that one field to descending without a separate reverse pass. Both are stable sorts; sort_unstable* variants trade stability for speed, and sort_by takes a full comparator returning an Ordering when a key function is not enough.

Python
from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

nums = [3, 1, 4, 1, 5, 9, 2, 6]
print("ascending:", sorted(nums))

people = [Person("Ada", 36), Person("Bo", 19), Person("Cy", 28)]
oldest = sorted(people, key=lambda p: p.age, reverse=True)
for p in oldest:
    print(f"{p.name} is {p.age}")

sorted returns a new list (the in-place list.sort is the mutating sibling). The key= argument is a key function called once per element to extract the value to compare - lambda p: p.age here - and reverse=True flips the result, so descending needs no negation trick. Python dropped the old cmp= comparator in version 3; the key-function style is now the one idiomatic way, with functools.cmp_to_key as the escape hatch.