Generics & Parametric Polymorphism
The same small task in every language: a generic container Box[T] that wraps a value of any type and carries a map from T to a new type U, plus a generic free function first that returns the first element of any list as an optional. Watch how the type parameter is introduced ([T] brackets vs. <T> angle brackets vs. 'a type variables vs. nothing at all in dynamic languages), and whether the reuse is checked at compile time - a statically-typed Box[Int] and Box[Str] share one definition but stay distinct types, while Perl and Python reuse the very same code with no type machinery.
“Laziness: The quality that makes you go to great effort to reduce overall energy expenditure. It makes you write labor-saving programs that other people will find useful, and document what you wrote so you don't have to answer so many questions about it.” - Larry Wall
# A generic container over any element type T
class Box[T] {
has $.value: T
# map carries T -> U, producing a Box of the new type
sub map($self, $f) {
Box(value: $f($self.value))
}
}
# A generic free function: the first element of any list, or None
sub first[T](@xs: List[T]): Option[T] {
if @xs.is_empty() { None } else { Some(@xs[0]) }
}
sub main() {
$b = Box(value: 21)
$doubled = $b.map({ $_ * 2 }) # Box[Int] -> Box[Int]
print("box holds { $doubled.value }")
$named = Box(value: "ada") # the same Box reused at Str
print("name box: { $named.value }")
@xs = [10, 20, 30]
$head = first(@xs).unwrap_or(0)
print("first of @xs is $head")
}Type parameters in Guji go in square brackets after the name - class Box[T] and sub first[T](...) - matching the List[T] / Option[T] syntax of the prelude. One Box definition serves every element type: Box(value: 21) is a Box[Int] and Box(value: "ada") a Box[Str], each statically distinct yet sharing the code. The compiler infers the type arguments from the constructor calls, so you write Box(value: 21) rather than Box[Int](value: 21), and map's $f is checked to carry the element type T to whatever U it returns.
package main
import "fmt"
// A generic container over any type T (Go 1.18+ type parameters).
type Box[T any] struct{ Value T }
// map is a free function: methods cannot add their own type params,
// so the T->U mapping is a top-level generic function.
func MapBox[T, U any](b Box[T], f func(T) U) Box[U] {
return Box[U]{Value: f(b.Value)}
}
// A generic free function: first element of any slice, or ok=false.
func First[T any](xs []T) (T, bool) {
var zero T
if len(xs) == 0 {
return zero, false
}
return xs[0], true
}
func main() {
b := Box[int]{Value: 21}
doubled := MapBox(b, func(x int) int { return x * 2 })
fmt.Println("box holds", doubled.Value)
named := Box[string]{Value: "ada"}
fmt.Println("name box:", named.Value)
xs := []int{10, 20, 30}
head, ok := First(xs)
if !ok {
head = 0
}
fmt.Println("first of", xs, "is", head)
}Go gained generics in 1.18 (2022): type parameters live in [T any] brackets, where any is the unconstrained bound. A notable limitation is that methods cannot introduce new type parameters, so the T-to-U transformation must be a top-level MapBox function rather than a method on Box. First returns the idiomatic Go (value, ok) pair instead of an Option, and a typed var zero T supplies the type's zero value when the slice is empty.
(* A generic container: 'a is a type variable, like T elsewhere. *)
type 'a box = { value : 'a }
(* map carries 'a -> 'b, producing a box of the new type. *)
let map_box (b : 'a box) (f : 'a -> 'b) : 'b box = { value = f b.value }
(* A generic free function: first element of any list, or None. *)
let first (xs : 'a list) : 'a option =
match xs with [] -> None | x :: _ -> Some x
let () =
let b = { value = 21 } in
let doubled = map_box b (fun x -> x * 2) in
Printf.printf "box holds %d\n" doubled.value;
let named = { value = "ada" } in
Printf.printf "name box: %s\n" named.value;
let xs = [10; 20; 30] in
let head = match first xs with Some x -> x | None -> 0 in
Printf.printf "first of [%s] is %d\n"
(String.concat "; " (List.map string_of_int xs)) headOCaml writes type parameters as lowercase type variables like 'a ("alpha") rather than uppercase T, and the whole thing is inferred: first is given the type 'a list -> 'a option with no annotations needed. Parametric polymorphism is OCaml's default - any function whose body never inspects a value's concrete type is automatically generic over it. The built-in option type (Some / None) is the direct ancestor of Guji's Option[T].
-- A generic container over any element type a.
newtype Box a = Box { value :: a }
-- mapBox carries a -> b, producing a Box of the new type.
mapBox :: (a -> b) -> Box a -> Box b
mapBox f (Box v) = Box (f v)
-- A generic free function: the first element of any list, or Nothing.
first :: [a] -> Maybe a
first [] = Nothing
first (x : _) = Just x
main :: IO ()
main = do
let doubled = mapBox (* 2) (Box (21 :: Int)) -- Box Int -> Box Int
putStrLn ("box holds " ++ show (value doubled))
let named = Box "ada" -- the same Box reused at String
putStrLn ("name box: " ++ value named)
let xs = [10, 20, 30] :: [Int]
head' = maybe 0 id (first xs)
putStrLn ("first of " ++ show xs ++ " is " ++ show head')Haskell type parameters are lowercase type variables like a and b, and parametric polymorphism is the default - any function not inspecting a concrete type is automatically generic, with Hindley-Milner inferring first :: [a] -> Maybe a for free. The built-in Maybe type (Just / Nothing) is the direct ancestor of Guji's Option[T]. Box here is really just fmap: making it an instance of Functor would let mapBox simply be fmap, the canonical map-over-a-container abstraction.
use strict;
use warnings;
# Perl is dynamically typed, so a Box already holds any value -
# "generics" need no type machinery; the same code reuses for all types.
package Box {
sub new { my ($class, $value) = @_; bless { value => $value }, $class }
sub value { $_[0]->{value} }
sub map {
my ($self, $f) = @_;
Box->new($f->($self->{value}));
}
}
# A generic free function: first element of any list, or undef.
sub first { my (@xs) = @_; @xs ? $xs[0] : undef }
my $b = Box->new(21);
my $doubled = $b->map(sub { $_[0] * 2 });
print "box holds ", $doubled->value, "\n";
my $named = Box->new("ada");
print "name box: ", $named->value, "\n";
my @xs = (10, 20, 30);
my $head = first(@xs);
print "first of [", join(", ", @xs), "] is ", (defined $head ? $head : 0), "\n";Perl is dynamically typed, so a container is implicitly generic: the same Box and first work for numbers and strings with no type parameters, bounds, or checks at all. Reuse comes for free, but the cost is that nothing prevents mixing types and any mistake surfaces only at run time. This is the opposite end of the spectrum from Guji, where Box[T] is one definition but Box[Int] and Box[Str] stay statically distinct.
# A generic container; Raku roles take type parameters in [ ].
role Box[::T] {
has T $.value;
method map(&f) { Box[$.value.WHAT].new(value => f($.value)) }
}
# A generic free function: first element of any list, or Nil.
sub first(@xs) { @xs ?? @xs[0] !! Nil }
my $b = Box[Int].new(value => 21);
my $doubled = $b.map(-> $x { $x * 2 });
say "box holds { $doubled.value }";
my $named = Box[Str].new(value => "ada");
say "name box: { $named.value }";
my @xs = 10, 20, 30;
my $head = first(@xs) // 0;
say "first of @xs[] is $head";Raku spells a parametric type as a parameterized role role Box[::T], where ::T declares the type variable and Box[Int] / Box[Str] instantiate it. Because Raku has gradual typing, you can stay generic with no annotations (as first does) or pin types down where it helps. The [ ] bracket syntax for type parameters is exactly the convention Guji adopts, making Raku the closest visible relative of Guji's Box[T].
// A generic container over any type T.
struct Box<T> {
value: T,
}
impl<T> Box<T> {
// map carries T -> U, consuming self and producing a Box<U>.
fn map<U>(self, f: impl Fn(T) -> U) -> Box<U> {
Box { value: f(self.value) }
}
}
// A generic free function: first element of any slice, or None.
fn first<T: Clone>(xs: &[T]) -> Option<T> {
xs.first().cloned()
}
fn main() {
let b = Box { value: 21 };
let doubled = b.map(|x| x * 2); // Box<i32> -> Box<i32>
println!("box holds {}", doubled.value);
let named = Box { value: "ada" }; // the same Box reused at &str
println!("name box: {}", named.value);
let xs = [10, 20, 30];
let head = first(&xs).unwrap_or(0);
println!("first of {:?} is {}", xs, head);
}Rust writes type parameters in angle brackets <T>, and a method may add its own (map<U>), unlike Go. Generics are monomorphized: the compiler emits a specialized copy of Box for each concrete type, so there is no runtime cost. Trait bounds like T: Clone on first state up front what operations a type parameter must support - the explicitness that Rust's ownership model and lack of a GC require.
from dataclasses import dataclass
from typing import Generic, TypeVar, Callable, Optional
T = TypeVar("T")
U = TypeVar("U")
# A generic container: typing is for the checker; it does not affect runtime.
@dataclass
class Box(Generic[T]):
value: T
def map(self, f: Callable[[T], U]) -> "Box[U]":
return Box(f(self.value))
def first(xs: list[T]) -> Optional[T]:
return xs[0] if xs else None
b = Box(21)
doubled = b.map(lambda x: x * 2)
print(f"box holds {doubled.value}")
named = Box("ada") # the same Box reused at str
print(f"name box: {named.value}")
xs = [10, 20, 30]
head = first(xs)
print(f"first of {xs} is {head if head is not None else 0}")Python is dynamically typed, so generics are purely an optional, erased annotation layer: TypeVar("T") with Generic[T] lets a type checker like mypy verify Box[int] versus Box[str], but at run time the same Box accepts anything regardless. The code is identical to an untyped version - Box(21) and Box("ada") both just work. This contrasts with Guji, where Box[T] is genuinely checked and erased at compile time rather than tracked at run time.