← History

Batteries Included or Bring Your Own: standard-library philosophies

How eight languages draw the line between what ships in the box and what you fetch from the ecosystem - and why that line is a design statement.

PythonGoRustHaskellOCamlPerlRakuGuji

A standard library is a promise. It says: here is what you can assume exists on every machine, in every version, without a package manager or a network connection. Where a language draws that line is one of the most revealing decisions its designers make. Draw it generously and you get "batteries included" - download once, build a web scraper before lunch. Draw it tightly and you get a small, sharp core that defers everything else to an ecosystem you curate yourself. Neither is wrong. They optimize for different fears.

The maximalists: Python and Perl

Python coined the phrase, and it earns it. The import this mantra of one obvious way to do things extends into a sprawling shelf of modules that ship with CPython: json, csv, sqlite3, http.server, argparse, collections, itertools, asyncio. You can do real work before touching pip.

from collections import Counter
words = "the cat the dog the bird".split()
print(Counter(words).most_common(1))  # [('the', 3)]

The cost is real, though. A large stdlib ages in public. Modules like urllib and asyncio carry the scars of decisions made decades apart, and the community routinely reaches past them for requests or httpx anyway. The battery is included, but everyone brought a spare.

Perl took the maximalist idea even further by treating CPAN as a de-facto extension of the core. Larry Wall liked to say the language was built to make easy things easy and hard things possible, and the stdlib plus CPAN was how it kept that bargain. The core itself is dense with text-handling power - hashes, regexes, and list operators that turn a tally into a one-liner:

my %c; $c{$_}++ for qw(the cat the dog the);
my ($top) = sort { $c{$b} <=> $c{$a} } keys %c;
say "$top -> $c{$top}";   # the -> 3

The deliberate middle: Go

Go is the most interesting case because it is generous and disciplined at once. Its standard library is famously complete for its domain: net/http is a production-grade server, encoding/json round-trips structs, crypto/*, database/sql, and testing all live in the box. A Go service can ship with zero third-party dependencies, and many do. That is a deliberate operational stance - fewer dependencies means fewer supply-chain surprises and a binary you can reason about.

But Go is conservative about what it admits. The standard library is covered by a compatibility promise, so things enter slowly and rarely leave. For years there was no official package for things like structured logging or generic slice helpers; the community filled the gap, and only later did log/slog, slices, and maps arrive. Go's philosophy is batteries included for the boring, load-bearing parts of a network service, and bring-your-own for the rest.

The minimalists: Rust and OCaml

Rust ships std with the essentials - collections, Result/Option, threads, basic IO - but deliberately keeps it small. There is no HTTP client, no JSON, no async runtime in the standard library. Instead, Cargo and crates.io are treated as first-class infrastructure, so serde, tokio, and reqwest feel almost standard despite living outside.

use std::collections::HashMap;
let mut c: HashMap<&str, i32> = HashMap::new();
for w in "the cat the dog the".split_whitespace() {
    *c.entry(w).or_insert(0) += 1;
}

The payoff is that the ecosystem can iterate fast without dragging the language's stability guarantees along with it. The async story could evolve in tokio without freezing a runtime into std forever. The risk is choice fatigue and a dependency tree that grows quickly. Rust accepts that trade because a good package manager makes "bring your own" cheap.

OCaml historically went even leaner. Its bundled standard library was small and idiosyncratic, which is exactly why the community built alternatives like Core and Batteries to replace it wholesale. With opam and dune as mature tooling, OCaml leans on curated ecosystem libraries rather than a fat core. The lesson mirrors Rust's: when the package manager is good, a thin stdlib is a feature, not a gap.

The purists: Haskell

Haskell is a special flavor of minimal. The base library and the Prelude give you the pure, lazy, type-class-driven core - Functor, Monad, Maybe, Either - and not much application plumbing. The Haskell Platform once tried to bundle common packages, but the center of gravity is Hackage and Stackage, fetched through Cabal or Stack. Because the language is pure and lazy, even mundane IO is mediated by the type system: getLine :: IO String is a value describing an effect, not the effect itself. The "battery" question becomes a type question, which is very on-brand for Haskell.

Raku: the kitchen-sink core

Raku (the language formerly aimed at by Perl 6) inherits Perl's maximalist instinct and bakes even more into the language proper. Grammars, junctions, lazy lists, a rational number type, and a full first-class regex/grammar engine are built in rather than imported. Its module ecosystem rides on zef, but the core itself is unusually rich. Raku's bet is that powerful built-in abstractions beat a thin core plus libraries.

Guji: a thin core with real platform IO

Guji (in-house, v0.1-alpha as of 2026-06-19) sits closer to the Rust/OCaml end - a compiled, statically typed, functional-first language with a reference interpreter and a native AOT compiler kept in parity. Its core is small and immutable-by-default, but it is not a toy: it ships real platform IO and concurrency rather than leaving those to libraries.

The standard surface is deliberately curated. Lists, maps, Option/Result, higher-order methods, first-class regex with named captures, and PEG grammars are all in the language. Strings interpolate with $var and { expr }, concatenate with ~, and the ? operator threads errors through Result:

sub halve($n: Int): Result[Int, Str] {
    if $n % 2 == 0 { Ok($n / 2) } else { Err("odd: $n") }
}

sub both($a: Int, $b: Int): Result[Int, Str] {
    $x = halve($a)?
    $y = halve($b)?
    Ok($x + $y)
}

Where Guji is unusually generous for its age is the platform layer. IO is not just print to stdout: there is note for stderr, args() returning List[Str], read_file($p): Result[Str, Str], open($p): Result[Handle, Str], a stdin handle with .slurp()/.lines(): Chan[Str], and exit($code). Concurrency is built in too, with hatch { } blocks and typed channels:

sub main() {
    $ch: Chan[Int] = channel()
    hatch {
        for $n in [1, 2, 3] { $ch.send($n * $n) }
        $ch.close()
    }
    mut $total = 0
    for $sq in $ch { $total = $total + $sq }
    print("sum of squares: $total")   // 14
}

Regex is first-class, returning an Option of a match object you destructure with match:

$email = "ada\@example.com"
match $email ~~ /(?<user>\w+)@(?<host>[\w.]+)/ {
    Some($m) { print("user={ $m<user>.unwrap_or('?') }") }
    None     { print("no match") }
}

So Guji's stance is its own: a thin, sharp, statically typed core - lists report length with .count(), bindings are immutable unless marked mut - but with the platform primitives (channels, file and stream IO, process args, exit codes) promoted into the language so that "hello, world" and "stream a file across goroutine-like tasks" both work without a single external dependency. It is bring-your-own for application libraries, batteries-included for the operating system.

The line is the lesson

Read these eight together and a pattern emerges. The size of a standard library is inversely correlated with the strength of the package manager. Python and Perl grew fat cores in an era before good dependency tooling; Rust, OCaml, and Guji can stay lean because Cargo, opam, and their equivalents make fetching cheap and reproducible. Go and Raku are the contrarians, betting that a strong, stable, batteries-included core is worth more than ecosystem velocity for the workloads they target. Where a language draws the line is not a measure of completeness. It is a statement about what it wants you to be able to trust.